Source code for fabulous.fabric_cad.timing_model.tools.synth_tools.yosys

"""Yosys Tool Interface, which uses Yosys to synthesize a Verilog design.

Converts a gate-level netlist and then uses an STA tool (e.g., OpenSTA) to analyze the
netlist and generate an SDF file.

The SDF file is then parsed to create a timing graph representation of the design. This
module provides a high-level interface for performing synthesis and timing analysis
using Yosys.
"""

import subprocess
from pathlib import Path

from loguru import logger

from fabulous.fabric_cad.timing_model.tools.specification import SynthTool


[docs] class YosysTool(SynthTool): """YosysTool is a synthesis tool interface that uses Yosys. It will synthesize Verilog RTL into a gate-level netlist. It supports various synthesis options such as techmapping, tie-high/low cell mapping, and buffer insertion. The generated gate-level netlist can then be used for static timing analysis (STA) with tools like OpenSTA. Initializes the YosysTool with the given configuration parameters. Parameters ---------- synth_executable : Path | str The path to the Yosys executable. top_name : str | None The name of the top-level module in the Verilog design. is_gate_level : bool Flag indicating whether the input Verilog files are already gate-level netlists (True) or need to be synthesized (False). techmap_files : list[Path] | None List of techmap files for Yosys or None if not using techmapping. tiehi_cell_and_port : str | None String specifying the tie-high cell and port for Yosys hilomap, or None if not using hilomap. tielo_cell_and_port : str | None String specifying the tie-low cell and port for Yosys hilomap, or None if not using hilomap. min_buf_cell_and_ports : str | None String specifying the minimum buffer cell and ports for Yosys insbuf, or None if not using insbuf. verilog_files : list[Path] | Path | None List of Verilog RTL files or a single Verilog RTL file to be synthesized. liberty_files : list[Path] | Path | None List of Liberty files or a single Liberty file for the target technology. debug : bool Flag indicating whether to enable debug mode for verbose output during synthesis. flat : bool Flag indicating whether to flatten the hierarchy during synthesis (default: False). """ def __init__( self, synth_executable: Path | str, top_name: str | None = None, is_gate_level: bool = False, techmap_files: list[Path] | None = None, tiehi_cell_and_port: str | None = None, tielo_cell_and_port: str | None = None, min_buf_cell_and_ports: str | None = None, verilog_files: list[Path] | Path | None = None, liberty_files: list[Path] | Path | None = None, debug: bool = False, flat: bool = False, ) -> None: self.verilog_files: list[Path] | Path | None = verilog_files self.lib_files: list[Path] | Path | None = liberty_files self.top_name: str | None = top_name self.synth_executable: Path | str = synth_executable self.techmap_files: list[Path] | None = techmap_files self.tiehi_cell_and_port: str | None = tiehi_cell_and_port self.tielo_cell_and_port: str | None = tielo_cell_and_port self.min_buf_cell_and_ports: str | None = min_buf_cell_and_ports self.is_gate_level: bool = is_gate_level self.debug: bool = debug self.flat: bool = flat self.netlist_path: Path | None = None
[docs] def synth_synthesize(self) -> None: """Generate a temporary gate-level netlist from the Verilog RTL files. It uses Yosys. The gate-level netlist is created in a temporary location and deleted after use. Returns ------- None If netlist provided is already gate-level. Raises ------ RuntimeError If synthesis fails or if the generated netlist file is empty or not created. """ self._check_errors() if self.is_gate_level: self.netlist_path = self.verilog_files return # Generate Yosys synthesis TCL script synth_tcl_script: str = "" synth_tcl_script += "yosys -import\n" if isinstance(self.lib_files, Path): synth_tcl_script += f"read_liberty -lib {self.lib_files}\n" else: for lib in self.lib_files: synth_tcl_script += f"read_liberty -lib {lib}\n" if isinstance(self.verilog_files, Path): synth_tcl_script += f"read_verilog -overwrite -sv {self.verilog_files}\n" else: for vf in self.verilog_files: synth_tcl_script += f"read_verilog -overwrite -sv {vf}\n" if self.flat: synth_tcl_script += f"synth -flatten -top {self.top_name}\n" else: synth_tcl_script += f"synth -top {self.top_name}\n" synth_tcl_script += f"renames -top {self.top_name}\n" synth_tcl_script += "renames -wire\n" if self.techmap_files is not None: for tm in self.techmap_files: synth_tcl_script += f"techmap -map {tm}\n" synth_tcl_script += "simplemap\n" synth_tcl_script += f"clockgate -liberty { self.lib_files[0] if isinstance(self.lib_files, list) else self.lib_files }\n" synth_tcl_script += f"dfflibmap -liberty { self.lib_files[0] if isinstance(self.lib_files, list) else self.lib_files }\n" synth_tcl_script += "setundef -zero\n" synth_tcl_script += "splitnets\n" if ( self.tiehi_cell_and_port is not None and self.tielo_cell_and_port is not None ): synth_tcl_script += ( f"hilomap -hicell {self.tiehi_cell_and_port} " f"-locell {self.tielo_cell_and_port}\n" ) if self.min_buf_cell_and_ports is not None: synth_tcl_script += f"insbuf -buf {self.min_buf_cell_and_ports}\n" synth_tcl_script += "tribuf\n" synth_tcl_script += f"abc -liberty { self.lib_files[0] if isinstance(self.lib_files, list) else self.lib_files }\n" synth_tcl_script += "opt -purge -full\n" synth_tcl_script += "write_verilog -noattr -noexpr {}\n".format( "{synth_output_file}" ) path: Path = Path.home() / ".fabulous" / "tmp" / f"synth_{self.top_name}_tmp.v" path.parent.mkdir(parents=True, exist_ok=True) logger.debug(f"Generating Synthesized Verilog file at temporary path: {path}") self._call_external( self.synth_executable, stdin_data=synth_tcl_script.format(synth_output_file=path), debug=self.debug, args=["-C"], ) content: str = path.read_text() if not content: path.unlink() raise RuntimeError( "Failed to generate gate-level netlist using Yosys. " "No content in netlist file." ) result_file: Path = path # Remove single-bit vector notation for compatibility # with OpenSTA SDF back-annotation netl: str = result_file.read_text() netl = netl.replace("[0:0]", " ") result_file.write_text(netl) self.netlist_path = result_file
@property
[docs] def synth_netlist_file(self) -> Path: """Return the path to the generated gate-level netlist file. Raises ------ RuntimeError If the netlist file has not been generated yet " "(i.e., synthesize() has not been called). """ if self.netlist_path is None: raise RuntimeError( "Netlist file has not been generated yet. Call synthesize() first." ) return self.netlist_path
@property
[docs] def synth_design_name(self) -> str: """Return the name of the design being synthesized. Returns ------- str The name of the design being synthesized. """ return self.top_name
@synth_design_name.setter def synth_design_name(self, name: str) -> None: """Set the name of the design being synthesized. Parameters ---------- name : str The name of the design being synthesized. """ self.top_name = name @property
[docs] def synth_rtl_files(self) -> list[Path] | Path: """Return the list of RTL Verilog files used for synthesis. Returns ------- list[Path] | Path The list of RTL Verilog files used for synthesis. """ return self.verilog_files
@synth_rtl_files.setter def synth_rtl_files(self, files: list[Path] | Path) -> None: """Set the list of RTL Verilog files used for synthesis. Parameters ---------- files : list[Path] | Path The list of RTL Verilog files to use for synthesis. """ self.verilog_files = files @property
[docs] def synth_liberty_files(self) -> list[Path] | Path: """Return the list of Liberty files used for synthesis. Returns ------- list[Path] | Path The list of Liberty files used for synthesis. """ return self.lib_files
@synth_liberty_files.setter def synth_liberty_files(self, files: list[Path] | Path) -> None: """Set the list of Liberty files used for synthesis. Parameters ---------- files : list[Path] | Path The list of Liberty files to use for synthesis. """ self.lib_files = files @property
[docs] def synth_passthrough(self) -> bool: """Return whether the synthesis tool is in passthrough mode. (i.e., it does not perform actual synthesis but simply passes through the input rtl files). Returns ------- bool True if the synthesis tool is in passthrough mode, False otherwise. """ return self.is_gate_level
@synth_passthrough.setter def synth_passthrough(self, value: bool) -> None: """Set whether the synthesis tool is in passthrough mode. Parameters ---------- value : bool True to enable passthrough mode, False to disable. """ self.is_gate_level = value
[docs] def synth_clean_up(self) -> None: """Clean up the temporary gate-level netlist file generated by Yosys.""" if self.netlist_path is not None and self.netlist_path.exists(): logger.debug(f"Cleaning up temporary netlist file at: {self.netlist_path}") # Dont delete the netlist file if it was provided # as input (i.e., gate-level netlist case) if not self.is_gate_level: self.netlist_path.unlink() self.netlist_path = None
def _call_external( self, executable: str, args: list[str] | None = None, stdin_data: str = "", debug: bool = False, ) -> subprocess.CompletedProcess: """Call an external executable with given arguments and stdin data. Captures the output and checks for errors. Parameters ---------- executable : str The path to the executable to run. args : list[str] | None List of arguments to pass to the executable. stdin_data : str Data to send to the executable's stdin. debug : bool If True, prints the command and stdin data for debugging purposes. Returns ------- subprocess.CompletedProcess The result of the subprocess call. Raises ------ RuntimeError If the external command fails. """ if args is None: args = [] if debug: logger.debug("Debug mode enabled for external command.") logger.debug(f"Calling external command: {executable} \n{' '.join(args)}") logger.debug(f"With stdin data:\n{stdin_data}") result = subprocess.run( [executable, *args], input=stdin_data, text=True, ) else: result = subprocess.run( [executable, *args], input=stdin_data, text=True, capture_output=True, check=False, ) if result.returncode != 0: raise RuntimeError( f"Command '{' '.join([executable, *args])}' failed with " f"error: {result.stderr}" ) return result def _check_errors(self) -> None: """Check YosysTool for errors in the configuration parameters. Helper method to validate the configuration parameters before running synthesis. It checks Raises ------ TypeError If any parameter has an incorrect type. FileNotFoundError If any specified file does not exist. ValueError If any specified file is empty or if there are invalid combinations of configuration values. """ if not isinstance(self.verilog_files, list | Path): raise TypeError( "verilog_files must be a list of pathlib.Path objects or a " "single pathlib.Path object." ) if self.is_gate_level and isinstance(self.verilog_files, list): raise TypeError( "If is_gate_level True, verilog_files must be a pathlib.Path obj." " Multiple Verilog files are not supported for gate-level netlists." ) if isinstance(self.verilog_files, list): for vf in self.verilog_files: if not isinstance(vf, Path): raise TypeError( "Each item in verilog_files list must be a pathlib.Path object." ) if not vf.exists(): raise FileNotFoundError(f"Verilog file not found: {vf}") if vf.stat().st_size == 0: raise ValueError(f"Verilog file is empty: {vf}") else: if not isinstance(self.verilog_files, Path): raise TypeError( "verilog_files must be a list of pathlib.Path objects or " "a single pathlib.Path object." ) if not self.verilog_files.exists(): raise FileNotFoundError(f"Verilog file not found: {self.verilog_files}") if self.verilog_files.stat().st_size == 0: raise ValueError(f"Verilog file is empty: {self.verilog_files}") if not isinstance(self.is_gate_level, bool): raise TypeError("is_gate_level must be a boolean value.") if self.is_gate_level and not isinstance(self.verilog_files, Path): raise TypeError( "When is_gate_level True, verilog_files must be a pathlib.Path object." " Multiple Verilog files are not supported for gate-level netlists." ) if not isinstance(self.synth_executable, Path | str): raise TypeError( "synth_executable must be a string or a pathlib.Path object." ) if self.techmap_files is not None: if not isinstance(self.techmap_files, list): raise TypeError( "techmap_files must be a list of pathlib.Path objects or None." ) for tm in self.techmap_files: if not isinstance(tm, Path): raise TypeError( "Each item in techmap_files list must be a pathlib.Path object." ) if not tm.exists(): raise FileNotFoundError(f"Techmap file not found: {tm}") if tm.stat().st_size == 0: raise ValueError(f"Techmap file is empty: {tm}") if not isinstance(self.tiehi_cell_and_port, str | type(None)): raise TypeError("tiehi_cell_and_port must be a string or None.") if not isinstance(self.tielo_cell_and_port, str | type(None)): raise TypeError("tielo_cell_and_port must be a string or None.") if not isinstance(self.min_buf_cell_and_ports, str | type(None)): raise TypeError("min_buf_cell_and_ports must be a string or None.") if not isinstance(self.flat, bool): raise TypeError("flat must be a boolean value.") if (self.tiehi_cell_and_port is None) ^ (self.tielo_cell_and_port is None): raise ValueError( "Both tiehi_cell_and_port and tielo_cell_and_port must " "be specified together." )