From 93e67c0380f24a83944de6270cb8b5d8c7f836e9 Mon Sep 17 00:00:00 2001 From: Max P Date: Sun, 27 Apr 2025 09:43:17 +0000 Subject: [PATCH] Adds testbench support and refines file handling Introduces a test process with commands to build and run testbenches. Adds utilities for generating simulation project files and resolving testbench sources. Updates file handling logic to support configurable destination directories. Improves organizational structure of CLI commands and enhances flexibility in managing project artifacts. --- src/hdlbuild/cli.py | 25 ++++- src/hdlbuild/tools/xilinx_ise/bitgen.py | 3 +- src/hdlbuild/tools/xilinx_ise/common.py | 5 +- src/hdlbuild/tools/xilinx_ise/isim.py | 140 ++++++++++++++++++++++++ src/hdlbuild/utils/source_resolver.py | 32 ++++++ 5 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 src/hdlbuild/tools/xilinx_ise/isim.py diff --git a/src/hdlbuild/cli.py b/src/hdlbuild/cli.py index a7c6130..74d2676 100644 --- a/src/hdlbuild/cli.py +++ b/src/hdlbuild/cli.py @@ -2,6 +2,8 @@ import argparse import sys from hdlbuild.dependencies.resolver import DependencyResolver +from hdlbuild.models.config import DIRECTORIES +from hdlbuild.tools.xilinx_ise.isim import build_testbench, generate_simulation_project_file, run_testbench from hdlbuild.tools.xilinx_ise.main import xilinx_ise_all, xilinx_ise_synth from hdlbuild.utils.console_utils import ConsoleUtils from hdlbuild.utils.directory_manager import clear_build_directories, clear_directories, ensure_directories_exist @@ -38,6 +40,12 @@ def dep(args): console_utils.print("Starting dependencies process...") DependencyResolver(project).resolve_all() +def test(args): + """Starts the test process.""" + console_utils.print("Starting test process...") + build_testbench(project, args.target) + run_testbench(args.target) + def main(): parser = argparse.ArgumentParser( @@ -67,12 +75,21 @@ def main(): parser_build.set_defaults(func=build) # Synth command - parser_build = subparsers.add_parser("synth", help="Start the synth process") - parser_build.set_defaults(func=synth) + parser_synth = subparsers.add_parser("synth", help="Start the synth process") + parser_synth.set_defaults(func=synth) # Dependencies command - parser_build = subparsers.add_parser("dep", help="Start the dependencies process") - parser_build.set_defaults(func=dep) + parser_dep = subparsers.add_parser("dep", help="Start the dependencies process") + parser_dep.set_defaults(func=dep) + + # Tests command + parser_test = subparsers.add_parser("test", help="Start the Tests process") + parser_test.set_defaults(func=test) + parser_test.add_argument( + "target", + nargs="?", + help="Select the target to test" + ) args = parser.parse_args() args.func(args) diff --git a/src/hdlbuild/tools/xilinx_ise/bitgen.py b/src/hdlbuild/tools/xilinx_ise/bitgen.py index 3eab141..f9b5cba 100644 --- a/src/hdlbuild/tools/xilinx_ise/bitgen.py +++ b/src/hdlbuild/tools/xilinx_ise/bitgen.py @@ -23,7 +23,8 @@ def copy_bitstream_file(project: ProjectConfig): copy_file( project=project, source_filename=f"{project.name}.bit", - destination_filename=f"{project.name}.Bitstream", + destination_filename=f"{project.name}.bit", + destination_dir=DIRECTORIES.copy_target, description="Bitstream File", step_number=10, total_steps=12 ) \ No newline at end of file diff --git a/src/hdlbuild/tools/xilinx_ise/common.py b/src/hdlbuild/tools/xilinx_ise/common.py index 2f83958..fc22dfc 100644 --- a/src/hdlbuild/tools/xilinx_ise/common.py +++ b/src/hdlbuild/tools/xilinx_ise/common.py @@ -46,6 +46,7 @@ def copy_file( source_filename: str, destination_filename: str, description: str = "Report", + destination_dir: str = DIRECTORIES.report, step_number: Optional[int] = None, total_steps: Optional[int] = None ): @@ -59,12 +60,12 @@ def copy_file( description (str): Optionale Beschreibung für die Ausgabe (z.B. "Synthesis Report") """ src_path = os.path.join(DIRECTORIES.build, source_filename) - dst_path = os.path.join(DIRECTORIES.report, destination_filename) + dst_path = os.path.join(destination_dir, destination_filename) if not os.path.exists(src_path): raise FileNotFoundError(f"{description} nicht gefunden: {src_path}") - os.makedirs(DIRECTORIES.report, exist_ok=True) + os.makedirs(destination_dir, exist_ok=True) shutil.copyfile(src_path, dst_path) diff --git a/src/hdlbuild/tools/xilinx_ise/isim.py b/src/hdlbuild/tools/xilinx_ise/isim.py new file mode 100644 index 0000000..d3a8350 --- /dev/null +++ b/src/hdlbuild/tools/xilinx_ise/isim.py @@ -0,0 +1,140 @@ +import os +from typing import List + +from hdlbuild.models.project import ProjectConfig +from hdlbuild.models.config import DIRECTORIES +from hdlbuild.dependencies.resolver import DependencyResolver +from hdlbuild.utils.console_utils import ConsoleTask +from hdlbuild.utils.source_resolver import expand_all_sources, expand_testbenches + +def generate_simulation_project_file(project: ProjectConfig, output_path: str, testbench_name: str): + """ + Generiert die ISim Simulationsprojektdatei (.prj). + + Args: + project (ProjectConfig): Das Hauptprojekt. + output_path (str): Zielpfad für die .prj Datei. + testbench_name (str): Name der Testbench-Datei (z.B. "VGATimingGenerator_test_tb"). + """ + resolver = DependencyResolver(project, offline_mode=True) + resolver.resolve_all() + + vhdl_sources, verilog_sources = expand_all_sources(project, resolver.resolved) + + with open(output_path, "w") as f: + # Normale VHDL-Sources + for lib, file in vhdl_sources: + f.write(f"vhdl {lib} \"{DIRECTORIES.get_relative_prefix()}{file}\"\n") + + # Normale Verilog-Sources + for lib, file in verilog_sources: + f.write(f"verilog {lib} \"{DIRECTORIES.get_relative_prefix()}{file}\"\n") + + # Testbench-Datei suchen und einfügen + testbench_file = find_testbench_file(project, testbench_name) + normalized_tb = os.path.normpath(testbench_file) + f.write(f"vhdl work \"{DIRECTORIES.get_relative_prefix()}{normalized_tb}\"\n") + + # glbl.v immer zuletzt + f.write(f"verilog work /opt/Xilinx/14.7/ISE_DS/ISE/verilog/src/glbl.v\n") + + +def find_testbench_file(project: ProjectConfig, testbench_name: str) -> str: + """ + Findet eine Testbench-Datei im Projekt anhand ihres Namens (ohne Endung, Case-Insensitive). + + Args: + project (ProjectConfig): Projektdefinition. + testbench_name (str): Gesuchter Dateiname (z.B. "VGATimingGenerator_test_tb"). + + Returns: + str: Vollständiger Pfad zur Testbench-Datei. + + Raises: + FileNotFoundError: Wenn die Datei nicht gefunden wurde. + """ + candidates = expand_testbenches(project) + + # Vergleichswerte vorbereiten (Name ohne Endung, in Kleinbuchstaben) + search_name = os.path.splitext(testbench_name)[0].lower() + + for _, filepath in candidates: + filename = os.path.basename(filepath) + filename_no_ext = os.path.splitext(filename)[0].lower() + + if filename_no_ext == search_name: + return filepath + + raise FileNotFoundError(f"Testbench '{testbench_name}' wurde nicht gefunden.") + +def build_testbench(project: ProjectConfig, testbench_name: str): + """ + Baut eine einzelne Testbench mit FUSE. + + Args: + project (ProjectConfig): Hauptprojekt-Konfiguration + testbench_name (str): Name der Testbench-Datei, z.B. "VGATimingGenerator_test_tb.vhd" + """ + # Pfade + isim_exe_name = f"isim_{testbench_name.replace('.vhd', '').replace('.v', '')}" + isim_exe_path = os.path.join(DIRECTORIES.build, isim_exe_name) + + # 1. Simulation-Projektdatei generieren + generate_simulation_project_file( + project=project, + output_path=os.path.join(DIRECTORIES.build, f"{project.name}_sim.prj"), + testbench_name=testbench_name + ) + + # 2. Befehl bauen + xilinx_path = project.xilinx_path + xilinx_bin_dir = os.path.join(xilinx_path, "bin", "lin64") # oder nt64 bei Windows + fuse_executable = os.path.join(xilinx_bin_dir, "fuse") + + cmd = [ + fuse_executable, + "-intstyle", "xflow", + "-prj", f"{project.name}_sim.prj", + "-o", isim_exe_name, + f"work.{testbench_name.replace('.vhd', '').replace('.v', '')}", + "work.glbl" + ] + + # 3. Ausführen mit Konsole + task = ConsoleTask(prefix="hdlbuild", title=f"FUSE {testbench_name}") + result = task.run_command(cmd, cwd=DIRECTORIES.build) + + if result != 0: + raise RuntimeError(f"FUSE fehlgeschlagen für Testbench {testbench_name}") + +def run_testbench(testbench_name: str): + """ + Führt eine gebaute Testbench-Executable aus (ISim Simulation). + + Args: + testbench_name (str): Name der Testbench-Datei (z.B. "VGATimingGenerator_test_tb.vhd") + """ + # Pfade + isim_exe_name = f"isim_{testbench_name.replace('.vhd', '').replace('.v', '')}" + isim_exe_path = os.path.join(DIRECTORIES.build, isim_exe_name) + + isim_cmd_file = os.path.join(DIRECTORIES.build, f"{isim_exe_name}.cmd") + + # 1. TCL-Skript für ISim erzeugen (einfache Simulation) + with open(isim_cmd_file, "w") as f: + f.write("") + + # 2. Kommando bauen + cmd = [ + f"./{isim_exe_name}", + "-gui", + "-tclbatch", + f"{isim_exe_name}.cmd" + ] + + # 3. Ausführen + task = ConsoleTask(prefix="hdlbuild", title=f"RUN {testbench_name}") + result = task.run_command(cmd, cwd=DIRECTORIES.build) + + if result != 0: + raise RuntimeError(f"Testbench {testbench_name} ist während der Simulation fehlgeschlagen!") \ No newline at end of file diff --git a/src/hdlbuild/utils/source_resolver.py b/src/hdlbuild/utils/source_resolver.py index e711ec5..ea6c7dc 100644 --- a/src/hdlbuild/utils/source_resolver.py +++ b/src/hdlbuild/utils/source_resolver.py @@ -66,3 +66,35 @@ def expand_all_sources(root_project: ProjectConfig, resolved_dependencies: List[ all_verilog_sources.extend(verilog_dep) return all_vhdl_sources, all_verilog_sources + + +def expand_testbenches(project: ProjectConfig) -> List[Tuple[str, str]]: + """ + Expandiert nur die Testbenches (vhdl und verilog) aus dem Hauptprojekt. + + Args: + project (ProjectConfig): Das Hauptprojekt. + + Returns: + List of (library, filepath) Tupel + """ + expanded = [] + + if project.testbenches: + # VHDL Testbenches + for source in project.testbenches.vhdl: + full_pattern = os.path.join(".", source.path) + matched_files = glob.glob(full_pattern, recursive=True) + for file in matched_files: + normalized = os.path.normpath(file) + expanded.append((source.library, normalized)) + + # Verilog Testbenches (optional) + for source in project.testbenches.verilog: + full_pattern = os.path.join(".", source.path) + matched_files = glob.glob(full_pattern, recursive=True) + for file in matched_files: + normalized = os.path.normpath(file) + expanded.append((source.library, normalized)) + + return expanded \ No newline at end of file