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.
This commit is contained in:
2025-04-27 09:43:17 +00:00
parent 8caafe862e
commit 93e67c0380
5 changed files with 198 additions and 7 deletions

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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!")

View File

@@ -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