Source code for fabulous.fabulous_repl.cmd_user_design

"""User design flow commands for the FABulous REPL.

Synthesis, place-and-route, bitstream generation, simulation, and user
design wrapper generation.
"""

import subprocess as sp
from pathlib import Path
from typing import Annotated, Literal

from cmd2 import with_annotated, with_category
from cmd2.annotated import Argument, Option
from loguru import logger

from fabulous.custom_exception import CommandError, InvalidFileType
from fabulous.fabulous_repl.command_set_base import CMD_USER_DESIGN_FLOW, ReplCommandSet
from fabulous.fabulous_repl.helper import make_hex, run_task
from fabulous.fabulous_settings import get_context


def _print_tool_help(tool_path: Path | str, args: list[str], tool_name: str) -> None:
    """Run a tool with the given arguments to print its help output.

    Parameters
    ----------
    tool_path : Path | str
        Path to the tool binary.
    args : list[str]
        Arguments to pass to the tool (e.g. ["-p", "help synth_fabulous"]).
    tool_name : str
        Human-readable tool name for error messages.
    """
    try:
        sp.run(
            [str(tool_path), *args],
            check=False,
        )
    except FileNotFoundError:
        logger.error(
            f"{tool_name} not found at '{tool_path}'. "
            "Ensure it is installed and on PATH."
        )


_SCL_BY_PDK: dict[str, str] = {
    "ihp-sg13g2": "sg13g2_stdcell",
    "sky130A": "sky130_fd_sc_hd",
    "gf180mcuD": "gf180mcu_fd_sc_mcu7t5v0",
}


[docs] def resolve_sim_libs(project: Path, overrides: list[str]) -> list[Path]: """Resolve the PDK standard-cell Verilog sim models for ``project``. Honours ``overrides`` (files or globs) first; otherwise takes the active PDK and its install root from the FABulous context (which resolves ``FAB_PDK`` / ``FAB_PDK_ROOT`` and the ciel install) and globs ``<pdk_root>/<pdk>/libs.ref/<scl>/verilog/`` for ``<scl>.v`` plus any ``*udp*.v`` / ``primitives.v`` companion (sky130 and gf180 ship their UDPs in a separate ``primitives.v``; IHP inlines them). Parameters ---------- project : Path Root of a hardened FABulous project; used to anchor relative override globs. overrides : list[str] Explicit sim-cell library files or globs. When non-empty, PDK auto-resolution is skipped. Returns ------- list[Path] Verilog cell-model files for the simulator. Raises ------ FileNotFoundError If an override matches nothing, or the resolved PDK sim file is missing. ValueError If the context has no PDK or PDK root, or the PDK has no known default standard-cell library. """ if overrides: resolved: list[Path] = [] for spec in overrides: path = Path(spec).expanduser() if path.is_file(): resolved.append(path.resolve()) continue anchor = path if path.is_absolute() else project / path matches = sorted(Path(anchor.anchor or "/").glob(str(anchor).lstrip("/"))) if not matches: raise FileNotFoundError(f"--gl-sim-libs {spec} matched no files") resolved.extend(m.resolve() for m in matches) return resolved ctx = get_context() pdk = ctx.pdk if not pdk: raise ValueError( "Cannot resolve PDK sim libs: set FAB_PDK in the project .env, or " "pass --gl-sim-libs explicitly." ) scl = _SCL_BY_PDK.get(pdk) if scl is None: raise ValueError( f"No default standard-cell library known for PDK '{pdk}'. Pass " "--gl-sim-libs to point at the cell models directly." ) if ctx.pdk_root is None: raise ValueError( f"Cannot resolve PDK_ROOT for '{pdk}'. Set FAB_PDK_ROOT in the " "project .env, install the PDK via ciel, or pass --gl-sim-libs." ) verilog_root = ctx.pdk_root / pdk / "libs.ref" / scl / "verilog" primary = verilog_root / f"{scl}.v" if not primary.exists(): raise FileNotFoundError(f"PDK sim file {primary} is missing.") companions = sorted( set(verilog_root.glob("*udp*.v")) | set(verilog_root.glob("primitives.v")) ) return [primary, *companions]
[docs] def collect_gl_sources(project: Path, sim_lib_overrides: list[str]) -> list[Path]: """Resolve every Verilog source the gate-level simulator needs. Returns one self-contained source list so the caller can hand it to iverilog directly (no extra ``find`` in the Taskfile): - the behavioural wrapper that keeps driving configuration (``Fabric/*.v``: ``eFPGA_top``, the config controller, ``Frame_*``, ``BlockRAM``, ``models_pack`` ...). The behavioural core ``eFPGA.v`` is excluded because the gate-level ``eFPGA.nl.v`` replaces it. - the post-PnR fabric netlist (``Fabric/macro/final_views`` holds exactly one ``*.nl.v``, structural, instantiating tile macros by name), - every tile netlist (``Tile/<tile>/macro/final_views/nl/<tile>.nl.v``), - the PDK cell models the netlists bind against. Parameters ---------- project : Path Root of a FABulous project hardened through the GDS flow. sim_lib_overrides : list[str] Explicit PDK sim-cell library files or globs; skips auto-resolution. Returns ------- list[Path] Behavioural wrapper, fabric netlist, tile netlists, then PDK cell models, in that order. Raises ------ FileNotFoundError If the fabric netlist or tile netlists are missing (the GDS flow has not been run). ValueError If more than one fabric netlist is present. """ macro_root = project / "Fabric" / "macro" / "final_views" fabric_netlists = sorted(macro_root.rglob("*.nl.v")) if not fabric_netlists: raise FileNotFoundError( f"No fabric netlist under {macro_root}. Run `gen_fabric_macro` " "against the project before gate-level simulation." ) if len(fabric_netlists) > 1: joined = ", ".join(str(p.relative_to(project)) for p in fabric_netlists) raise ValueError(f"Multiple fabric netlists; refusing to guess: {joined}") tile_netlists = sorted((project / "Tile").glob("*/macro/final_views/nl/*.nl.v")) if not tile_netlists: raise FileNotFoundError( f"No tile netlists under {project / 'Tile'}/*/macro/final_views/nl/. " "Run `gen_all_tile_macros` first." ) # Behavioural wrapper Verilog directly under Fabric/ (not the macro/ tree). # Exclude the behavioural fabric core; the gate-level netlist replaces it. behavioural = sorted( p for p in (project / "Fabric").glob("*.v") if p.name != "eFPGA.v" ) sim_libs = resolve_sim_libs(project, sim_lib_overrides) logger.info( f"GL sources: {len(behavioural)} behavioural wrapper + 1 fabric netlist " f"+ {len(tile_netlists)} tile netlists + {len(sim_libs)} PDK sim file(s)" ) return [*behavioural, *fabric_netlists, *tile_netlists, *sim_libs]
[docs] class UserDesignCommandSet(ReplCommandSet): """User design flow: synthesis through bitstream and simulation.""" @with_category(CMD_USER_DESIGN_FLOW) @with_annotated
[docs] def do_synthesis( # noqa: PLR0913, C901 self, files: Annotated[list[Path], Argument(help_text="Verilog source files")], top: Annotated[str, Option("-top")] = "top_wrapper", auto_top: Annotated[bool, Option("-auto-top")] = False, blif: Annotated[Path | None, Option("-blif")] = None, edif: Annotated[Path | None, Option("-edif")] = None, json_file: Annotated[Path | None, Option("-json")] = None, lut: Annotated[str, Option("-lut")] = "4", plib: Annotated[str | None, Option("-plib")] = None, extra_plib: Annotated[ list[Path] | None, Option("-extra-plib", action="append") ] = None, extra_map: Annotated[ list[Path] | None, Option("-extra-map", action="append") ] = None, encfile: Annotated[Path | None, Option("-encfile")] = None, nofsm: Annotated[bool, Option("-nofsm")] = False, noalumacc: Annotated[bool, Option("-noalumacc")] = False, carry: Annotated[Literal["none", "ha"], Option("-carry")] = "none", noregfile: Annotated[bool, Option("-noregfile")] = False, iopad: Annotated[bool, Option("-iopad")] = False, complex_dff: Annotated[bool, Option("-complex-dff")] = False, noflatten: Annotated[bool, Option("-noflatten")] = False, nordff: Annotated[bool, Option("-nordff")] = False, noshare: Annotated[bool, Option("-noshare")] = False, run: Annotated[str | None, Option("-run")] = None, no_rw_check: Annotated[bool, Option("-no-rw-check")] = False, ) -> None: """Run Yosys synthesis for the specified Verilog files. deprecated: Use ``compile_design --synth-only`` instead. """ repl = self._cmd del auto_top # accepted for backwards compat; not forwarded to compile_design logger.warning( "The 'synthesis' command is deprecated. Use 'compile_design' instead." ) # Translate legacy flags into --synth-extra-args for compile_design extra = [] if blif: extra.append(f"-blif {blif}") if edif: extra.append(f"-edif {edif}") if lut: extra.append(f"-lut {lut}") if plib: extra.append(f"-plib {plib}") if extra_plib: extra.extend(f"-extra-plib {p}" for p in extra_plib) if extra_map: extra.extend(f"-extra-map {m}" for m in extra_map) if encfile: extra.append(f"-encfile {encfile}") if nofsm: extra.append("-nofsm") if noalumacc: extra.append("-noalumacc") if carry and carry != "none": extra.append(f"-carry {carry}") if noregfile: extra.append("-noregfile") if iopad: extra.append("-iopad") if complex_dff: extra.append("-complex-dff") if noflatten: extra.append("-noflatten") if nordff: extra.append("-nordff") if noshare: extra.append("-noshare") if run: extra.append(f"-run {run}") if no_rw_check: extra.append("-no-rw-check") cmd = f"compile_design {' '.join(str(f) for f in files)} --synth-only" if top != "top_wrapper": cmd += f" -top {top}" if json_file: cmd += f" -json {json_file}" if extra: cmd += f' --synth-extra-args "{" ".join(extra)}"' repl.onecmd_plus_hooks(cmd)
@with_category(CMD_USER_DESIGN_FLOW) @with_annotated
[docs] def do_place_and_route( self, file: Annotated[Path, Argument(help_text="Path to the target file")], ) -> None: """Run place and route with Nextpnr for a given JSON file. deprecated: Use ``compile_design --pnr-only`` instead. """ repl = self._cmd logger.warning( "The 'place_and_route' command is deprecated. " "Use 'compile_design --pnr-only' instead." ) if file.suffix != ".json": raise InvalidFileType( "No json file provided. Usage: place_and_route <json_file>" ) repl.onecmd_plus_hooks(f"compile_design {file} --pnr-only")
@with_category(CMD_USER_DESIGN_FLOW) @with_annotated
[docs] def do_gen_bitStream_binary( self, file: Annotated[Path, Argument(help_text="Path to the target file")], ) -> None: """Generate bitstream of a given design. deprecated: Use ``compile_design`` which includes bitstream generation. """ repl = self._cmd logger.warning( "The 'gen_bitStream_binary' command is deprecated. " "Use 'compile_design' instead, which includes bitstream generation." ) if file.suffix != ".fasm": raise InvalidFileType( "No fasm file provided. Usage: gen_bitStream_binary <fasm_file>" ) repl.onecmd_plus_hooks(f"compile_design {file} --bitgen-only")
@with_category(CMD_USER_DESIGN_FLOW) @with_annotated
[docs] def do_run_FABulous_bitstream( self, file: Annotated[Path, Argument(help_text="Path to the target file")], ) -> None: """Run FABulous to generate bitstream on a given design. deprecated: Use ``compile_design`` instead. """ repl = self._cmd logger.warning( "The 'run_FABulous_bitstream' command is deprecated. " "Use 'compile_design' instead." ) if file.suffix not in [".v", ".sv"]: raise InvalidFileType( "No Verilog or SystemVerilog file provided. " "Usage: run_FABulous_bitstream <top_module_file>" ) repl.onecmd_plus_hooks(f"compile_design {file}")
# Overrides cmd2's built-in run_script (which cmd2 also invokes internally for # the `@` relative-script shortcut). Because it shadows a built-in, it cannot # be moved into a CommandSet, which cmd2 forbids from replacing existing # attributes. @with_category(CMD_USER_DESIGN_FLOW) @with_annotated
[docs] def do_gen_user_design_wrapper( self, user_design: Annotated[Path, Argument(help_text="Path to user design file")], user_design_top_wrapper: Annotated[ Path, Argument(help_text="Output path for user design top wrapper") ], ) -> None: """Generate a wrapper that connects the user design to the fabric.""" repl = self._cmd if not repl.fabric_loaded: raise CommandError("Need to load fabric first") project_dir = get_context().proj_dir repl.fabulousAPI.generateUserDesignTopWrapper( project_dir / user_design, project_dir / user_design_top_wrapper, )
@with_category(CMD_USER_DESIGN_FLOW) @with_annotated
[docs] def do_compile_design( self, files: Annotated[list[Path], Argument(help_text="Path to the target files.")], top: Annotated[ str, Option( "-top", help_text=( "Use the specified module as the top module " "(default='top_wrapper')." ), ), ] = "top_wrapper", json: Annotated[ Path | None, Option( "-json", help_text=( "Write the design to the specified JSON file. " "If not specified, defaults to <first_file_stem>.json." ), ), ] = None, synth_only: Annotated[ bool, Option("--synth-only", help_text="Only run synthesis.") ] = False, pnr_only: Annotated[ bool, Option( "--pnr-only", help_text="Only run place-and-route (JSON must already exist).", ), ] = False, bitgen_only: Annotated[ bool, Option( "--bitgen-only", help_text="Only run bitstream generation (FASM must already exist).", ), ] = False, synth_extra_args: Annotated[ str, Option( "--synth-extra-args", help_text=( "Extra arguments appended to the synth_fabulous command " "(e.g. '-nofsm -extra-plib prims.v')." ), ), ] = "", yosys_extra_args: Annotated[ str, Option( "--yosys-extra-args", help_text=( "Extra arguments passed to the Yosys CLI itself " "(before the -p flag)." ), ), ] = "", nextpnr_extra_args: Annotated[ str, Option( "--nextpnr-extra-args", help_text="Extra arguments passed to the nextpnr CLI.", ), ] = "", log: Annotated[ Path | None, Option("-log", help_text="Set log output file path") ] = None, fasm: Annotated[ Path | None, Option("-fasm", help_text="Set fasm output file path") ] = None, bin_file: Annotated[ Path | None, Option("-bin", help_text="Set bit file output file path") ] = None, yosys_synth_help: Annotated[ bool, Option( "--yosys-synth-help", help_text="Print the full synth_fabulous help from Yosys and exit.", ), ] = False, nextpnr_help: Annotated[ bool, Option("--nextpnr-help", help_text="Print the full nextpnr help and exit."), ] = False, ) -> None: """Compile a user design through synthesis, PnR, and bitstream generation. This function orchestrates the full compile flow by delegating to a compile Taskfile. It resolves input file paths, builds the synthesis command, and invokes the appropriate task(s) depending on the selected mode (full compile, synth-only, pnr-only, or no-bitgen). """ repl = self._cmd if yosys_synth_help: ctx = get_context() _print_tool_help(ctx.yosys_path, ["-p", "help synth_fabulous"], "Yosys") return if nextpnr_help: ctx = get_context() _print_tool_help(ctx.nextpnr_path, ["--help"], "nextpnr") return logger.info(f"Compiling design with files {[str(i) for i in files]}") p: Path paths: list[Path] = [] for p in files: if not p.is_absolute(): p = repl.projectDir / p resolvePath: Path = p.absolute() if resolvePath.exists(): paths.append(resolvePath) else: logger.error(f"{resolvePath} does not exist") return # Output paths must be absolute: the task runs with cwd=.FABulous/. json_file = json or paths[0].with_suffix(".json") if not json_file.is_absolute(): json_file = (repl.projectDir / json_file).resolve() fasm_file = fasm or json_file.with_suffix(".fasm") if not fasm_file.is_absolute(): fasm_file = (repl.projectDir / fasm_file).resolve() log_file = log or json_file.parent / ( json_file.with_suffix("").name + "_npnr_log.txt" ) if not log_file.is_absolute(): log_file = (repl.projectDir / log_file).resolve() bin_file = bin_file or fasm_file.with_suffix(".bin") if not bin_file.is_absolute(): bin_file = (repl.projectDir / bin_file).resolve() task_dir = repl.projectDir / "Test" compile_taskfile = task_dir / "Taskfile.yml" if not compile_taskfile.exists(): raise FileNotFoundError( f"Compile Taskfile not found at {compile_taskfile}. " "Please ensure the project is set up correctly." ) ctx = get_context() task_vars: dict[str, str] = { "YOSYS_PATH": str(ctx.yosys_path), "NEXTPNR_PATH": str(ctx.nextpnr_path), "FAB_PROJ_ROOT": str(repl.projectDir), "DESIGN": paths[0].stem, "TOP_WRAPPER": top, "DESIGN_FILES": " ".join(str(p) for p in paths), "TOP_WRAPPER_FILE": str(repl.projectDir / "user_design" / "top_wrapper.v"), "JSON_FILE": str(json_file), "FASM_FILE": str(fasm_file), "BIN_FILE": str(bin_file), "LOG_FILE": str(log_file), "SYNTH_EXTRA_ARGS": synth_extra_args, "YOSYS_EXTRA_ARGS": yosys_extra_args, "NEXTPNR_EXTRA_ARGS": nextpnr_extra_args, "NEXTPNR_VERBOSE": "--verbose" if (repl.verbose or repl.debug) else "", } if synth_only: run_task("run-yosys", task_dir, task_vars) elif pnr_only: run_task("run-nextpnr", task_dir, task_vars) elif bitgen_only: run_task("run-bitgen", task_dir, task_vars) else: run_task("build-test-design", task_dir, task_vars) logger.info("Compile flow completed successfully.")
@with_category(CMD_USER_DESIGN_FLOW) @with_annotated
[docs] def do_run_simulation( self, output_format: Annotated[ Literal["vcd", "fst"], Argument( help_text="Output format of the simulation", metavar="format", ), ], file: Annotated[Path, Argument(help_text="Path to the bitstream file")], design: Annotated[ str, Option( "--design", "-d", help_text=( "Design name to simulate " "(default: inferred from bitstream filename)" ), ), ] = "", simulator: Annotated[ Literal["nvc", "ghdl", "auto", ""], Option( "--simulator", "-s", help_text=( "VHDL simulator to use: nvc, ghdl, or auto (default: auto-detect)" ), ), ] = "", extra_iverilog_flag: Annotated[ str, Option( "--extra-iverilog-flag", "-if", help_text="Extra flags to pass to iverilog (Verilog projects)", ), ] = "", extra_nvc_flag: Annotated[ str, Option( "--extra-nvc-flag", "-nf", help_text="Extra flags to pass to NVC (VHDL projects)", ), ] = "", extra_ghdl_flag: Annotated[ str, Option( "--extra-ghdl-flag", "-gf", help_text="Extra flags to pass to GHDL (VHDL projects)", ), ] = "", gl: Annotated[ bool, Option( "--gl", help_text=( "Gate-level (mixed-level) simulation: keep the behavioural " "wrapper but swap the fabric core for the hardened post-PnR " "netlist. Verilog only; the project must have been run " "through `gen_fabric_macro`." ), ), ] = False, gl_sim_libs: Annotated[ list[str] | None, Option( "--gl-sim-libs", action="append", metavar="FILE_OR_GLOB", help_text=( "Verilog sim-cell library file or glob (repeatable). Overrides PDK " "auto-resolution from FAB_PDK / FAB_PDK_ROOT. Only used with --gl." ), ), ] = None, ) -> None: """Simulate given FPGA design. Uses Taskfile.yml (preferred) or falls back to Make (deprecated). The bitstream_file argument should be a binary file generated by 'compile_design'. With ``--gl`` the hardened fabric netlist replaces the behavioural core for gate-level simulation. """ repl = self._cmd if file.is_relative_to(repl.projectDir): bitstream_path = file else: bitstream_path = repl.projectDir / file if bitstream_path.suffix != ".bin": raise InvalidFileType( "No bitstream file specified. " "Usage: run_simulation <format> <bitstream_file>" ) if not bitstream_path.exists(): raise FileNotFoundError( f"Cannot find {bitstream_path} file which is generated by running " "compile_design. Potentially the bitstream generation failed." ) test_path = repl.projectDir / "Test" taskfile = test_path / "Taskfile.yml" makefile = test_path / "Makefile" design_name = design or bitstream_path.stem # Prepare build directory and convert .bin to .hex for simulation build_dir = test_path / "build" build_dir.mkdir(parents=True, exist_ok=True) hex_path = build_dir / f"{design_name}.hex" make_hex(bitstream_path, hex_path) logger.info(f"Converted {bitstream_path} to {hex_path}") task_vars = { "WAVEFORM_TYPE": output_format, "DESIGN": design_name, "BITSTREAM_BIN": str(bitstream_path.resolve()), } if simulator: task_vars["SIMULATOR"] = simulator if extra_iverilog_flag: task_vars["EXTRA_IVERILOG_FLAGS"] = extra_iverilog_flag if extra_nvc_flag: task_vars["EXTRA_NVC_FLAGS"] = extra_nvc_flag if extra_ghdl_flag: task_vars["EXTRA_GHDL_FLAGS"] = extra_ghdl_flag if gl: if repl.extension == "vhdl": raise InvalidFileType( "Gate-level simulation is Verilog-only: the hardened netlists " "and PDK cell models are Verilog, which nvc/ghdl cannot " "co-simulate with a VHDL wrapper." ) if not taskfile.exists(): raise FileNotFoundError( f"No Taskfile.yml found in {test_path}. Gate-level simulation " "requires the Taskfile flow." ) gl_sources = collect_gl_sources(repl.projectDir, gl_sim_libs or []) task_vars["GL_SOURCES"] = " ".join(str(p) for p in gl_sources) logger.info(f"Running gate-level simulation for {design_name} via Taskfile") run_task( "run-gl-simulation", task_dir=test_path, task_vars=task_vars, verbose=repl.verbose or repl.debug, ) logger.info("Gate-level simulation finished") return if taskfile.exists(): logger.info(f"Running simulation for {design_name} via Taskfile") run_task( "run-simulation", task_dir=test_path, task_vars=task_vars, verbose=repl.verbose or repl.debug, ) elif makefile.exists(): logger.warning( "Taskfile.yml not found, falling back to Makefile. " "Makefiles are deprecated and will be removed in the next release. " "Please migrate to Taskfile.yml." ) make_cmd = ["make", "-C", str(test_path), "run_simulation"] if repl.verbose or repl.debug: logger.info(f"Running command: {' '.join(make_cmd)}") sp.run(make_cmd, check=True) else: raise FileNotFoundError( f"No Taskfile.yml or Makefile found in {test_path}. " "Please ensure the project Test directory is set up correctly." ) logger.info("Simulation finished")