"""Run-simulation command implementation for the FABulous CLI.
RTL simulation exercises the behavioural fabric FABulous emits. Gate-level
simulation (``--gl``) reuses the *same* testbench and bitstream but swaps the
inner fabric core ``eFPGA`` and its tiles for the post-place-and-route netlists
hardened by the GDS flow, linked against the PDK standard-cell models. The
behavioural wrapper ``eFPGA_top`` (with its configuration controller) is kept,
so the existing ``<design>_tb.v`` drives the mixed-level DUT unchanged.
"""
import argparse
import subprocess as sp
from pathlib import Path
from typing import TYPE_CHECKING
from cmd2 import Cmd, Cmd2ArgumentParser, with_argparser, with_category
from loguru import logger
from fabulous.custom_exception import InvalidFileType
from fabulous.fabulous_cli.helper import make_hex, run_task
from fabulous.fabulous_settings import get_context
if TYPE_CHECKING:
from fabulous.fabulous_cli.fabulous_cli import FABulous_CLI
[docs]
CMD_USER_DESIGN_FLOW = "User Design Flow"
# Default PDK standard-cell library per PDK; override with --gl-sim-libs for a
# PDK or install layout not covered here.
_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]
run_simulation_parser = Cmd2ArgumentParser()
run_simulation_parser.add_argument(
"format",
choices=["vcd", "fst"],
default="fst",
help="Output format of the simulation",
)
run_simulation_parser.add_argument(
"file",
type=Path,
completer=Cmd.path_complete,
help="Path to the bitstream file",
)
run_simulation_parser.add_argument(
"-d",
"--design",
default="",
help="Design name to simulate (default: inferred from bitstream filename)",
)
run_simulation_parser.add_argument(
"-s",
"--simulator",
default="",
choices=["nvc", "ghdl", "auto", ""],
help="VHDL simulator to use: nvc, ghdl, or auto (default: auto-detect)",
)
run_simulation_parser.add_argument(
"-if",
"--extra-iverilog-flag",
default="",
help="Extra flags to pass to iverilog (Verilog projects)",
)
run_simulation_parser.add_argument(
"-nf",
"--extra-nvc-flag",
default="",
help="Extra flags to pass to NVC (VHDL projects)",
)
run_simulation_parser.add_argument(
"-gf",
"--extra-ghdl-flag",
default="",
help="Extra flags to pass to GHDL (VHDL projects)",
)
run_simulation_parser.add_argument(
"--gl",
action="store_true",
help="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`.",
)
run_simulation_parser.add_argument(
"--gl-sim-libs",
action="append",
default=[],
metavar="FILE_OR_GLOB",
help="Verilog sim-cell library file or glob (repeatable). Overrides PDK "
"auto-resolution from FAB_PDK / FAB_PDK_ROOT. Only used with --gl.",
)
@with_category(CMD_USER_DESIGN_FLOW)
@with_argparser(run_simulation_parser)
[docs]
def do_run_simulation(self: "FABulous_CLI", args: argparse.Namespace) -> 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.
"""
if args.file.is_relative_to(self.projectDir):
bitstreamPath = args.file
else:
bitstreamPath = self.projectDir / args.file
if bitstreamPath.suffix != ".bin":
raise InvalidFileType(
"No bitstream file specified. "
"Usage: run_simulation <format> <bitstream_file>"
)
if not bitstreamPath.exists():
raise FileNotFoundError(
f"Cannot find {bitstreamPath} file which is generated by running "
"compile_design. Potentially the bitstream generation failed."
)
testPath = self.projectDir / "Test"
taskfile = testPath / "Taskfile.yml"
makefile = testPath / "Makefile"
design_name = args.design or bitstreamPath.stem
# Prepare build directory and convert .bin to .hex for simulation
buildDir = testPath / "build"
buildDir.mkdir(parents=True, exist_ok=True)
hexPath = buildDir / f"{design_name}.hex"
make_hex(bitstreamPath, hexPath)
logger.info(f"Converted {bitstreamPath} to {hexPath}")
task_vars = {
"WAVEFORM_TYPE": args.format,
"DESIGN": design_name,
"BITSTREAM_BIN": str(bitstreamPath.resolve()),
}
if args.simulator:
task_vars["SIMULATOR"] = args.simulator
if args.extra_iverilog_flag:
task_vars["EXTRA_IVERILOG_FLAGS"] = args.extra_iverilog_flag
if args.extra_nvc_flag:
task_vars["EXTRA_NVC_FLAGS"] = args.extra_nvc_flag
if args.extra_ghdl_flag:
task_vars["EXTRA_GHDL_FLAGS"] = args.extra_ghdl_flag
if args.gl:
if self.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 {testPath}. Gate-level simulation "
"requires the Taskfile flow."
)
gl_sources = collect_gl_sources(self.projectDir, args.gl_sim_libs)
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=testPath,
task_vars=task_vars,
verbose=self.verbose or self.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=testPath,
task_vars=task_vars,
verbose=self.verbose or self.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(testPath), "run_simulation"]
if self.verbose or self.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 {testPath}. "
"Please ensure the project Test directory is set up correctly."
)
logger.info("Simulation finished")