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:
@@ -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)
|
||||
|
@@ -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
|
||||
)
|
@@ -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)
|
||||
|
||||
|
140
src/hdlbuild/tools/xilinx_ise/isim.py
Normal file
140
src/hdlbuild/tools/xilinx_ise/isim.py
Normal 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!")
|
@@ -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
|
Reference in New Issue
Block a user