diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index 2f74c9ac..5e6a4631 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -31,7 +31,7 @@ import traceback from contextlib import redirect_stdout from glob import glob -from pathlib import PurePosixPath, PureWindowsPath, Path +from pathlib import PurePath, Path from typing import List, Literal from dotenv import load_dotenv @@ -252,6 +252,37 @@ def make_hex(binfile, outfile): print("0", file=f) +def check_if_application_exists(application: str, throw_exception: bool = True) -> Path: + """Checks if an application is installed on the system. + + Parameters + ---------- + application : str + Name of the application to check. + throw_exception : bool, optional + If True, throws an exception if the application is not installed, by default True + + Returns + ------- + Path + Path to the application, if installed. + + Raises + ------ + Exception + If the application is not installed and throw_exception is True. + """ + path = shutil.which(application) + if path is not None: + return Path(path) + else: + logger.error( + f"{application} is not installed. Please install it or set FAB__PATH in the .env file." + ) + if throw_exception: + raise Exception(f"{application} is not installed.") + + def adjust_directory_in_verilog_tb(project_dir): """Adjusts directory paths in a Verilog testbench file by replacing the string "PROJECT_DIR" in the project_template with the actual @@ -271,35 +302,6 @@ def adjust_directory_in_verilog_tb(project_dir): fout.write(line.replace("PROJECT_DIR", f"{project_dir}")) -def get_path(path): - """Returns system-specific path object. - - Parameters - ---------- - path : str - File system path. - - Returns - ------- - PurePath - System-specific path object (PurePosixPath or PureWindowsPath) - - Raises - ------ - NotImplementedError - If the operating system is not supported. - """ - system = platform.system() - # Darwin corresponds to MacOS, which also uses POSIX-style paths - if system == "Linux" or system == "Darwin": - return PurePosixPath(path) - elif system == "Windows": - return PureWindowsPath(path) - else: - logger.error(f"Unsupported operating system: {system}") - raise NotImplementedError - - class PlaceAndRouteError(Exception): """An exception to be thrown when place and route fails.""" @@ -1168,7 +1170,7 @@ def do_synthesis(self, args): f"do_synthesis takes exactly one argument ({len(args)} given)" ) logger.info(f"Running synthesis that targeting Nextpnr with design {args[0]}") - path = get_path(args[0]) + path = PurePath(args[0]) parent = path.parent verilog_file = path.name top_module_name = path.stem @@ -1182,8 +1184,9 @@ def do_synthesis(self, args): return json_file = top_module_name + ".json" + yosys = check_if_application_exists(os.getenv("FAB_YOSYS_PATH", "yosys")) runCmd = [ - f"{os.getenv('FAB_YOSYS_PATH', 'yosys')}", + f"{yosys}", "-p", f"synth_fabulous -top top_wrapper -json {self.projectDir}/{parent}/{json_file}", f"{self.projectDir}/{parent}/{verilog_file}", @@ -1231,7 +1234,7 @@ def do_place_and_route(self, args): f"do_place_and_route takes exactly one argument ({len(args)} given)" ) logger.info(f"Running Placement and Routing with Nextpnr for design {args[0]}") - path = get_path(args[0]) + path = PurePath(args[0]) parent = path.parent json_file = path.name top_module_name = path.stem @@ -1261,10 +1264,13 @@ def do_place_and_route(self, args): if os.path.exists(f"{self.projectDir}/{parent}"): # TODO rewriting the fab_arch script so no need to copy file for work around + npnr = check_if_application_exists( + os.getenv("FAB_NEXTPNR_PATH", "nextpnr-generic") + ) if f"{json_file}" in os.listdir(f"{self.projectDir}/{parent}"): runCmd = [ f"FAB_ROOT={self.projectDir}", - f"{os.getenv('FAB_NEXTPNR_PATH', 'nextpnr-generic')}", + f"{npnr}", "--uarch", "fabulous", "--json", @@ -1321,7 +1327,7 @@ def do_gen_bitStream_binary(self, args): if len(args) != 1: logger.error("Usage: gen_bitStream_binary ") return - path = get_path(args[0]) + path = PurePath(args[0]) parent = path.parent fasm_file = path.name top_module_name = path.stem @@ -1454,9 +1460,12 @@ def do_run_simulation(self, args): os.path.join(tmp_dir, filename) for filename in os.listdir(tmp_dir) ] + iverilog = check_if_application_exists( + os.getenv("FAB_IVERILOG_PATH", "iverilog") + ) try: runCmd = [ - f"os.getenv('FAB_IVERILOG_PATH', 'iverilog')", + f"{iverilog}", "-D", f"{defined_option}", "-s", @@ -1479,9 +1488,10 @@ def do_run_simulation(self, args): f"{self.projectDir}/{path}/{bitstream_hex}", ) + vvp = check_if_application_exists(os.getenv("FAB_VVP_PATH", "vvp")) try: runCmd = [ - f"{os.getenv('FAB_VVP_PATH', 'vvp')}", + f"{vvp}", f"{self.projectDir}/{path}/{vvp_file}", ] sp.run(runCmd, check=True) @@ -1509,11 +1519,26 @@ def do_run_FABulous_bitstream(self, *args): run_FABulous_bitstream """ - if len(args) != 1: + if len(args) == 1: + verilog_file_path = PurePath(args[0]) + elif len(args) == 2: + # Backwards compatibility to older scripts + if "npnr" in args[0]: + verilog_file_path = PurePath(args[1]) + elif "vpr" in args[0]: + logger.error( + "run_FABulous_bitstream does not support vpr anymore, please use npnr or try an older FABulous version." + ) + return + + else: + logger.error(f"run_FABulous_bitstream does not support {args[0]}") + return + + else: logger.error("Usage: run_FABulous_bitstream ") return - verilog_file_path = get_path(args[0]) file_path_no_suffix = verilog_file_path.parent / verilog_file_path.stem if verilog_file_path.suffix != ".v": @@ -1554,7 +1579,7 @@ def do_tcl(self, args): logger.error("Usage: tcl ") return path_str = args[0] - path = get_path(path_str) + path = PurePath(path_str) name = path.stem if not os.path.exists(path_str): logger.error(f"Cannot find {path_str}") diff --git a/FABulous/fabric_generator/fabric_gen.py b/FABulous/fabric_generator/fabric_gen.py index 2ec27371..b16cc7ed 100644 --- a/FABulous/fabric_generator/fabric_gen.py +++ b/FABulous/fabric_generator/fabric_gen.py @@ -19,7 +19,6 @@ import csv import math import os -import pathlib import re import string from sys import prefix @@ -69,7 +68,7 @@ def __init__(self, fabric: Fabric, writer: codeGenerator) -> None: self.writer = writer @staticmethod - def bootstrapSwitchMatrix(tile: Tile, outputDir: pathlib.Path) -> None: + def bootstrapSwitchMatrix(tile: Tile, outputDir: Path) -> None: """Generates a blank switch matrix CSV file for the given tile. The top left corner will contain the name of the tile. Columns are the source signals and rows are the destination signals. @@ -117,7 +116,7 @@ def bootstrapSwitchMatrix(tile: Tile, outputDir: pathlib.Path) -> None: writer.writerow([p] + [0] * len(destName)) @staticmethod - def list2CSV(InFileName: pathlib.Path, OutFileName: pathlib.Path) -> None: + def list2CSV(InFileName: Path, OutFileName: Path) -> None: """This function is used to export a given list description into its equivalent CSV switch matrix description. A comment will be appended to the end of the column and row of the matrix, which will indicate the number of signals in a given row. @@ -2425,10 +2424,21 @@ def generateBitsStreamSpec(self) -> Dict[str, Dict]: for x, tile in enumerate(row): if tile == None: continue - configMemPath = tile.tileDir.parent.joinpath( - f"{tile.name}_ConfigMem.csv" - ) - if configMemPath.exists(): + if "fabric.csv" in str(tile.tileDir): + # backward compatibility for old project structure + configMemPath = ( + Path(os.getenv("FAB_PROJ_DIR")) + / "Tile" + / tile.name + / f"{tile.name}_ConfigMem.csv" + ) + else: + configMemPath = tile.tileDir.parent.joinpath( + f"{tile.name}_ConfigMem.csv" + ) + logger.info(f"ConfigMemPath: {configMemPath}") + + if configMemPath.exists() and configMemPath.is_file(): configMemList = parseConfigMem( configMemPath, self.fabric.maxFramesPerCol, @@ -2439,6 +2449,10 @@ def generateBitsStreamSpec(self) -> Dict[str, Dict]: logger.critical( f"No ConfigMem csv file found for {tile.name} which have config bits" ) + configMemList = [] + else: + logger.info(f"No config memory for {tile.name}.") + configMemList = [] encodeDict = [-1] * ( self.fabric.maxFramesPerCol * self.fabric.frameBitsPerRow diff --git a/FABulous/fabric_generator/file_parser.py b/FABulous/fabric_generator/file_parser.py index 3d1bfb77..9a6bbd9a 100644 --- a/FABulous/fabric_generator/file_parser.py +++ b/FABulous/fabric_generator/file_parser.py @@ -1,18 +1,15 @@ import csv -import os -import pathlib import re import subprocess import json -from sys import prefix from loguru import logger from copy import deepcopy from typing import Literal, overload +from pathlib import Path from FABulous.fabric_generator.utilities import expandListPorts from FABulous.fabric_definition.Bel import Bel from FABulous.fabric_definition.Port import Port -from FABulous.fabric_definition.Wire import Wire from FABulous.fabric_definition.Tile import Tile from FABulous.fabric_definition.SuperTile import SuperTile from FABulous.fabric_definition.Fabric import Fabric @@ -59,7 +56,7 @@ def parseFabricCSV(fileName: str) -> Fabric: The fabric object. """ - fName = pathlib.Path(fileName) + fName = Path(fileName) if fName.suffix != ".csv": logger.error("File must be a csv file") raise ValueError @@ -239,12 +236,12 @@ def parseFabricCSV(fileName: str) -> Fabric: ) -def parseMatrix(fileName: pathlib.Path, tileName: str) -> dict[str, list[str]]: +def parseMatrix(fileName: Path, tileName: str) -> dict[str, list[str]]: """Parse the matrix CSV into a dictionary from destination to source. Parameters ---------- - fileName : pathlib.Path + fileName : Path Directory of the matrix CSV file. tileName : str Name of the tile needed to be parsed. @@ -286,20 +283,20 @@ def parseMatrix(fileName: pathlib.Path, tileName: str) -> dict[str, list[str]]: @overload def parseList( - filePath: pathlib.Path, collect: Literal["pair"] = "pair", prefix: str = "" + filePath: Path, collect: Literal["pair"] = "pair", prefix: str = "" ) -> list[tuple[str, str]]: pass @overload def parseList( - filePath: pathlib.Path, collect: Literal["source", "sink"], prefix: str = "" + filePath: Path, collect: Literal["source", "sink"], prefix: str = "" ) -> dict[str, list[str]]: pass def parseList( - filePath: pathlib.Path, + filePath: Path, collect: Literal["pair", "source", "sink"] = "pair", prefix: str = "", ) -> list[tuple[str, str]] | dict[str, list[str]]: @@ -307,7 +304,7 @@ def parseList( Parameters ---------- - fileName : pathlib.Path + fileName : Path "" collect : (Literal["", "source", "sink"], optional) Collect value by source, sink or just as pair. Defaults to "pair". @@ -442,7 +439,7 @@ def parsePortLine(line: str) -> tuple[list[Port], tuple[str, str] | None]: return (ports, commonWirePair) -def parseTiles(fileName: pathlib.Path) -> tuple[list[Tile], list[tuple[str, str]]]: +def parseTiles(fileName: Path) -> tuple[list[Tile], list[tuple[str, str]]]: """Parses a CSV tile configuration file and returns all tile objects. Parameters @@ -455,7 +452,6 @@ def parseTiles(fileName: pathlib.Path) -> tuple[list[Tile], list[tuple[str, str] Tuple[List[Tile], List[Tuple[str, str]]] A tuple containing a list of Tile objects and a list of common wire pairs. """ - logger.info(f"Reading tile configuration: {fileName}") if fileName.suffix != ".csv": @@ -483,7 +479,7 @@ def parseTiles(fileName: pathlib.Path) -> tuple[list[Tile], list[tuple[str, str] tileName = t[0].split(",")[1] ports: list[Port] = [] bels: list[Bel] = [] - matrixDir: pathlib.Path | None = None + matrixDir: Path | None = None withUserCLK = False configBit = 0 for item in t: @@ -555,7 +551,6 @@ def parseTiles(fileName: pathlib.Path) -> tuple[list[Tile], list[tuple[str, str] else: logger.error(f"Unknown tile description {temp[0]} in tile {t}.") raise ValueError - withUserCLK = any(bel.withUserCLK for bel in bels) new_tiles.append( Tile( @@ -572,9 +567,7 @@ def parseTiles(fileName: pathlib.Path) -> tuple[list[Tile], list[tuple[str, str] return (new_tiles, commonWirePairs) -def parseSupertiles( - fileName: pathlib.Path, tileDic: dict[str, Tile] -) -> list[SuperTile]: +def parseSupertiles(fileName: Path, tileDic: dict[str, Tile]) -> list[SuperTile]: """Parses a CSV supertile configuration file and returns all SuperTile objects. Parameters @@ -660,7 +653,7 @@ def parseSupertiles( def parseBelFile( - filename: pathlib.Path, + filename: Path, belPrefix: str = "", filetype: Literal["verilog", "vhdl"] = "", ) -> Bel: @@ -1061,7 +1054,7 @@ def vhdl_belMapProcessing(file: str, filename: str) -> dict: def parseConfigMem( - fileName: pathlib.Path, + fileName: Path, maxFramePerCol: int, frameBitPerRow: int, globalConfigBits: int,