From 91f4f03d97d8ba8b8f4eab9a8ab0790b4b81893d Mon Sep 17 00:00:00 2001 From: Max P Date: Sat, 26 Apr 2025 13:47:50 +0000 Subject: [PATCH] Introduces support for Xilinx ISE build flow Adds scripts and utilities for synthesis, mapping, placement, and bitstream generation using Xilinx ISE tools. Refactors configuration management into a dedicated module. Updates project model to support tool-specific options. Adjusts `.gitignore` and Python version compatibility. Simplifies directory handling and ensures modularity by reorganizing configuration and tool logic. --- .gitignore | 3 +- pyproject.toml | 2 +- src/config.py | 14 ----- src/hdlbuild.py | 37 ++++++++++- src/models/config.py | 26 ++++++++ src/models/project.py | 11 ++++ src/tools/xilinx_ise/bitgen.py | 63 +++++++++++++++++++ src/tools/xilinx_ise/common.py | 54 +++++++++++++++++ src/tools/xilinx_ise/map.py | 67 ++++++++++++++++++++ src/tools/xilinx_ise/ngdbuild.py | 46 ++++++++++++++ src/tools/xilinx_ise/par.py | 85 ++++++++++++++++++++++++++ src/tools/xilinx_ise/xst.py | 101 +++++++++++++++++++++++++++++++ src/utils/directory_manager.py | 2 +- 13 files changed, 493 insertions(+), 18 deletions(-) delete mode 100644 src/config.py create mode 100644 src/models/config.py create mode 100644 src/tools/xilinx_ise/bitgen.py create mode 100644 src/tools/xilinx_ise/common.py create mode 100644 src/tools/xilinx_ise/map.py create mode 100644 src/tools/xilinx_ise/ngdbuild.py create mode 100644 src/tools/xilinx_ise/par.py create mode 100644 src/tools/xilinx_ise/xst.py diff --git a/.gitignore b/.gitignore index 7e7a9ca..ccc4962 100644 --- a/.gitignore +++ b/.gitignore @@ -183,4 +183,5 @@ output/ vhdl/ poetry.lock project.yml -.project/ \ No newline at end of file +.project/ +.devcontainer/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index cf45865..ff86dc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" readme = "README.md" [tool.poetry.dependencies] -python = "^3.12" +python = "^3.10" pyyaml = "^6.0.2" doit = "^0.36.0" pydantic = "^2.11.3" diff --git a/src/config.py b/src/config.py deleted file mode 100644 index 44d7be8..0000000 --- a/src/config.py +++ /dev/null @@ -1,14 +0,0 @@ -from pydantic import BaseModel - -class DirectoryConfig(BaseModel): - dependency: str = ".hdlbuild_deps" - build: str = ".working" - report: str = "reports" - copy_target: str = "output" - -DIRECTORIES = DirectoryConfig() - -class GitConfig(BaseModel): - timeout: int = 10 - -GIT = GitConfig() \ No newline at end of file diff --git a/src/hdlbuild.py b/src/hdlbuild.py index a692fa5..9a0d0a4 100644 --- a/src/hdlbuild.py +++ b/src/hdlbuild.py @@ -1,3 +1,9 @@ +from models.config import DIRECTORIES +from tools.xilinx_ise.bitgen import copy_bitstream_file, run_bitgen +from tools.xilinx_ise.map import copy_map_report, run_map +from tools.xilinx_ise.ngdbuild import run_ngdbuild +from tools.xilinx_ise.par import copy_par_report, copy_pinout_report, run_par +from tools.xilinx_ise.xst import copy_synthesis_report, generate_xst_project_file, generate_xst_script_file, run_xst from utils.directory_manager import clear_directories, ensure_directories_exist from utils.project_loader import load_project_config from utils.source_resolver import expand_sources @@ -7,11 +13,40 @@ project = load_project_config() print(project.name) print(project.sources.vhdl) +clear_directories() + ensure_directories_exist() -clear_directories() + expanded_vhdl = expand_sources(project.sources.vhdl) for library, filepath in expanded_vhdl: print(f"vhdl {library} \"{filepath}\"") + +generate_xst_project_file(project, f"{DIRECTORIES.build}/{project.name}.prj") +generate_xst_script_file(project, f"{DIRECTORIES.build}/{project.name}.scr") +print(f"XST project file generated at {DIRECTORIES.build}/{project.name}.prj") +print(f"XST script file generated at {DIRECTORIES.build}/{project.name}.scr") + +run_xst(project) +print("Run XST") + +copy_synthesis_report(project) +print("Copy synthesis report") + +run_ngdbuild(project) +print("Run ngdbuild") + +run_map(project) +print("Run map") +copy_map_report(project) +print("Copy map report") + +run_par(project) +print("Run par") +copy_par_report(project) +copy_pinout_report(project) + +run_bitgen(project) +copy_bitstream_file(project) \ No newline at end of file diff --git a/src/models/config.py b/src/models/config.py new file mode 100644 index 0000000..f780d13 --- /dev/null +++ b/src/models/config.py @@ -0,0 +1,26 @@ +import os +from pydantic import BaseModel + +class DirectoryConfig(BaseModel): + dependency: str = ".hdlbuild_deps" + build: str = ".working" + report: str = "reports" + copy_target: str = "output" + + def get_relative_prefix(self) -> str: + """ + Gibt den relativen Pfad von build-Verzeichnis zurück zum Hauptverzeichnis. + + Beispiel: + ".working" -> "../" + ".build/deep" -> "../../" + """ + depth = len(os.path.normpath(self.build).split(os.sep)) + return "../" * depth + +DIRECTORIES = DirectoryConfig() + +class GitConfig(BaseModel): + timeout: int = 10 + +GIT = GitConfig() \ No newline at end of file diff --git a/src/models/project.py b/src/models/project.py index a2ee897..3969179 100644 --- a/src/models/project.py +++ b/src/models/project.py @@ -5,6 +5,16 @@ class SourceFile(BaseModel): path: str library: str = "work" # Default auf 'work' +class ToolOptions(BaseModel): + common: List[str] = Field(default_factory=list) + xst: List[str] = Field(default_factory=list) + ngdbuild: List[str] = Field(default_factory=list) + map: List[str] = Field(default_factory=list) + par: List[str] = Field(default_factory=list) + bitgen: List[str] = Field(default_factory=list) + trace: List[str] = Field(default_factory=list) + fuse: List[str] = Field(default_factory=list) + class Dependency(BaseModel): name: str git: str @@ -34,3 +44,4 @@ class ProjectConfig(BaseModel): constraints: Optional[str] = None build: Optional[BuildOptions] = None dependencies: Optional[List[Dependency]] = Field(default_factory=list) + tool_options: Optional[ToolOptions] = ToolOptions() diff --git a/src/tools/xilinx_ise/bitgen.py b/src/tools/xilinx_ise/bitgen.py new file mode 100644 index 0000000..2bfb66a --- /dev/null +++ b/src/tools/xilinx_ise/bitgen.py @@ -0,0 +1,63 @@ +import subprocess +import os +import shutil +from typing import Optional +from models.project import ProjectConfig +from models.config import DIRECTORIES + +def run_bitgen(project: ProjectConfig, working_dir: Optional[str] = None): + """ + Führt Xilinx BitGen aus, um den finalen Bitstream zu erzeugen. + + Args: + project (ProjectConfig): Geladene Projektkonfiguration. + working_dir (str, optional): Arbeitsverzeichnis; Standard: build-Verzeichnis. + """ + if working_dir is None: + working_dir = DIRECTORIES.build + + xilinx_bin_dir = os.path.join(project.xilinx_path, "bin", "lin64") # oder "nt64" für Windows + bitgen_executable = os.path.join(xilinx_bin_dir, "bitgen") + + if not os.path.exists(bitgen_executable): + raise FileNotFoundError(f"BitGen-Executable nicht gefunden unter: {bitgen_executable}") + + print(f"[hdlbuild] Starte BitGen über {bitgen_executable}") + print(f"[hdlbuild] Arbeitsverzeichnis: {working_dir}") + + cmd = [bitgen_executable] + + # Füge zuerst die "common" Optionen ein (falls vorhanden) + if project.tool_options and project.tool_options.common: + cmd.extend(project.tool_options.common) + + # Dann die BitGen-spezifischen Optionen + if project.tool_options and project.tool_options.bitgen: + cmd.extend(project.tool_options.bitgen) + + # Dann die Pflicht-Argumente + cmd.extend([ + "-w", + f"{project.name}.ncd", + f"{project.name}.bit" + ]) + + subprocess.run(cmd, cwd=working_dir, check=True) + +def copy_bitstream_file(project: ProjectConfig): + """ + Kopiert die Bitstream-Datei (.bit) vom Build-Verzeichnis ins Output-Verzeichnis. + + Args: + project (ProjectConfig): Geladene Projektkonfiguration. + """ + src_path = os.path.join(DIRECTORIES.build, f"{project.name}.bit") + dst_path = os.path.join(DIRECTORIES.copy_target, f"{project.name}.bit") + + if not os.path.exists(src_path): + raise FileNotFoundError(f"Bitstream-Datei nicht gefunden: {src_path}") + + os.makedirs(DIRECTORIES.copy_target, exist_ok=True) + + shutil.copyfile(src_path, dst_path) + print(f"[hdlbuild] Bitstream-Datei kopiert nach {dst_path}") \ No newline at end of file diff --git a/src/tools/xilinx_ise/common.py b/src/tools/xilinx_ise/common.py new file mode 100644 index 0000000..953b707 --- /dev/null +++ b/src/tools/xilinx_ise/common.py @@ -0,0 +1,54 @@ +import subprocess +import os +from typing import Optional, List +from models.project import ProjectConfig +from models.config import DIRECTORIES + +def run_tool( + project: ProjectConfig, + tool_executable_name: str, + tool_option_attr: str, + mandatory_arguments: List[str], + working_dir: Optional[str] = None +): + """ + Führt ein beliebiges Xilinx ISE Tool aus (XST, NGDBuild, MAP, PAR, BitGen), + mit Common- und Toolspezifischen Optionen + festen Pflichtargumenten. + + Args: + project (ProjectConfig): Das Projekt-Objekt + tool_executable_name (str): z.B. "xst", "map", "par", "bitgen" + tool_option_attr (str): Attribut-Name in tool_options, z.B. "xst", "map" + mandatory_arguments (List[str]): Liste mit Pflicht-Argumenten + working_dir (str, optional): Arbeitsverzeichnis + """ + if working_dir is None: + working_dir = DIRECTORIES.build + + xilinx_bin_dir = os.path.join(project.xilinx_path, "bin", "lin64") # oder "nt64" + tool_executable = os.path.join(xilinx_bin_dir, tool_executable_name) + + if not os.path.exists(tool_executable): + raise FileNotFoundError(f"Executable nicht gefunden: {tool_executable}") + + print(f"[hdlbuild] Starte {tool_executable_name.upper()} über {tool_executable}") + print(f"[hdlbuild] Arbeitsverzeichnis: {working_dir}") + + cmd = [tool_executable] + + # Füge zuerst "common" Optionen ein + if project.tool_options and project.tool_options.common: + cmd.extend(project.tool_options.common) + + # Füge dann Toolspezifische Optionen ein + if project.tool_options: + tool_opts = getattr(project.tool_options, tool_option_attr, []) + if tool_opts: + cmd.extend(tool_opts) + + # Füge die Pflicht-Argumente an + cmd.extend(mandatory_arguments) + + print(f"[hdlbuild] Befehl: {' '.join(cmd)}") + + subprocess.run(cmd, cwd=working_dir, check=True) diff --git a/src/tools/xilinx_ise/map.py b/src/tools/xilinx_ise/map.py new file mode 100644 index 0000000..52ac0be --- /dev/null +++ b/src/tools/xilinx_ise/map.py @@ -0,0 +1,67 @@ +import subprocess +import os +import shutil +from typing import Optional +from models.project import ProjectConfig +from models.config import DIRECTORIES + +def run_map(project: ProjectConfig, working_dir: Optional[str] = None): + """ + Führt Xilinx MAP aus, basierend auf dem gegebenen Projekt. + + Args: + project (ProjectConfig): Geladene Projektkonfiguration. + working_dir (str, optional): Arbeitsverzeichnis; Standard: build-Verzeichnis. + """ + if working_dir is None: + working_dir = DIRECTORIES.build + + xilinx_bin_dir = os.path.join(project.xilinx_path, "bin", "lin64") # oder "nt64" für Windows + map_executable = os.path.join(xilinx_bin_dir, "map") + + if not os.path.exists(map_executable): + raise FileNotFoundError(f"MAP-Executable nicht gefunden unter: {map_executable}") + + print(f"[hdlbuild] Starte MAP über {map_executable}") + print(f"[hdlbuild] Arbeitsverzeichnis: {working_dir}") + + cmd = [map_executable] + + # Füge zuerst die "common" Optionen ein (falls vorhanden) + if project.tool_options and project.tool_options.common: + cmd.extend(project.tool_options.common) + + # Dann die MAP-spezifischen Optionen + if project.tool_options and project.tool_options.map: + cmd.extend(project.tool_options.map) + + # Dann die Pflicht-Argumente + cmd.extend([ + "-p", project.target_device, + "-w", + f"{project.name}.ngd", + "-o", f"{project.name}.map.ncd", + f"{project.name}.pcf" + ]) + + + subprocess.run(cmd, cwd=working_dir, check=True) + +def copy_map_report(project: ProjectConfig): + """ + Kopiert den Map-Report (.map.mrp) vom Build-Verzeichnis ins Report-Verzeichnis + und benennt ihn sinnvoll um. + + Args: + project (ProjectConfig): Geladene Projektkonfiguration. + """ + src_path = os.path.join(DIRECTORIES.build, f"{project.name}.map.mrp") + dst_path = os.path.join(DIRECTORIES.report, f"{project.name}.MapReport") + + if not os.path.exists(src_path): + raise FileNotFoundError(f"Map-Report nicht gefunden: {src_path}") + + os.makedirs(DIRECTORIES.report, exist_ok=True) + + shutil.copyfile(src_path, dst_path) + print(f"[hdlbuild] Map-Report kopiert nach {dst_path}") \ No newline at end of file diff --git a/src/tools/xilinx_ise/ngdbuild.py b/src/tools/xilinx_ise/ngdbuild.py new file mode 100644 index 0000000..677096f --- /dev/null +++ b/src/tools/xilinx_ise/ngdbuild.py @@ -0,0 +1,46 @@ +import subprocess +import os +from typing import Optional +from models.project import ProjectConfig +from models.config import DIRECTORIES + +def run_ngdbuild(project: ProjectConfig, working_dir: Optional[str] = None): + """ + Führt Xilinx NGDBuild aus, basierend auf dem gegebenen Projekt. + + Args: + project (ProjectConfig): Geladene Projektkonfiguration. + working_dir (str, optional): Arbeitsverzeichnis; Standard: build-Verzeichnis. + """ + if working_dir is None: + working_dir = DIRECTORIES.build + + xilinx_bin_dir = os.path.join(project.xilinx_path, "bin", "lin64") # oder "nt64" für Windows + ngdbuild_executable = os.path.join(xilinx_bin_dir, "ngdbuild") + + if not os.path.exists(ngdbuild_executable): + raise FileNotFoundError(f"NGDBuild-Executable nicht gefunden unter: {ngdbuild_executable}") + + print(f"[hdlbuild] Starte NGDBuild über {ngdbuild_executable}") + print(f"[hdlbuild] Arbeitsverzeichnis: {working_dir}") + + cmd = [ngdbuild_executable] + + # Füge zuerst die "common" Optionen ein (falls vorhanden) + if project.tool_options and project.tool_options.common: + cmd.extend(project.tool_options.common) + + # Dann die NGDBuild-spezifischen Optionen + if project.tool_options and project.tool_options.ngdbuild: + cmd.extend(project.tool_options.ngdbuild) + + # Dann die Pflicht-Argumente + cmd.extend([ + "-p", project.target_device, + "-uc", f"{DIRECTORIES.get_relative_prefix()}{project.constraints}", + f"{project.name}.ngc", + f"{project.name}.ngd" + ]) + + + subprocess.run(cmd, cwd=working_dir, check=True) diff --git a/src/tools/xilinx_ise/par.py b/src/tools/xilinx_ise/par.py new file mode 100644 index 0000000..fba6cf8 --- /dev/null +++ b/src/tools/xilinx_ise/par.py @@ -0,0 +1,85 @@ +import subprocess +import shutil +import os +from typing import Optional +from models.project import ProjectConfig +from models.config import DIRECTORIES + +def run_par(project: ProjectConfig, working_dir: Optional[str] = None): + """ + Führt Xilinx PAR (Place & Route) aus, basierend auf dem gegebenen Projekt. + + Args: + project (ProjectConfig): Geladene Projektkonfiguration. + working_dir (str, optional): Arbeitsverzeichnis; Standard: build-Verzeichnis. + """ + if working_dir is None: + working_dir = DIRECTORIES.build + + xilinx_bin_dir = os.path.join(project.xilinx_path, "bin", "lin64") # oder "nt64" für Windows + par_executable = os.path.join(xilinx_bin_dir, "par") + + if not os.path.exists(par_executable): + raise FileNotFoundError(f"PAR-Executable nicht gefunden unter: {par_executable}") + + print(f"[hdlbuild] Starte PAR über {par_executable}") + print(f"[hdlbuild] Arbeitsverzeichnis: {working_dir}") + + cmd = [par_executable] + + # Füge zuerst die "common" Optionen ein (falls vorhanden) + if project.tool_options and project.tool_options.common: + cmd.extend(project.tool_options.common) + + # Dann die PAR-spezifischen Optionen + if project.tool_options and project.tool_options.par: + cmd.extend(project.tool_options.par) + + # Dann die Pflicht-Argumente + cmd.extend([ + "-w", + f"{project.name}.map.ncd", + f"{project.name}.ncd", + f"{project.name}.pcf" + ]) + + + subprocess.run(cmd, cwd=working_dir, check=True) + +def copy_par_report(project: ProjectConfig): + """ + Kopiert den Place & Route Report (.par) vom Build-Verzeichnis ins Report-Verzeichnis + und benennt ihn sinnvoll um. + + Args: + project (ProjectConfig): Geladene Projektkonfiguration. + """ + src_path = os.path.join(DIRECTORIES.build, f"{project.name}.par") + dst_path = os.path.join(DIRECTORIES.report, f"{project.name}.PlaceRouteReport") + + if not os.path.exists(src_path): + raise FileNotFoundError(f"PAR-Report nicht gefunden: {src_path}") + + os.makedirs(DIRECTORIES.report, exist_ok=True) + + shutil.copyfile(src_path, dst_path) + print(f"[hdlbuild] PAR-Report kopiert nach {dst_path}") + +def copy_pinout_report(project: ProjectConfig): + """ + Kopiert den Pinout Summary Report (_pad.txt) vom Build-Verzeichnis ins Report-Verzeichnis + und benennt ihn sinnvoll um. + + Args: + project (ProjectConfig): Geladene Projektkonfiguration. + """ + src_path = os.path.join(DIRECTORIES.build, f"{project.name}_pad.txt") + dst_path = os.path.join(DIRECTORIES.report, f"{project.name}.PinoutReport") + + if not os.path.exists(src_path): + raise FileNotFoundError(f"Pinout-Report nicht gefunden: {src_path}") + + os.makedirs(DIRECTORIES.report, exist_ok=True) + + shutil.copyfile(src_path, dst_path) + print(f"[hdlbuild] Pinout-Report kopiert nach {dst_path}") \ No newline at end of file diff --git a/src/tools/xilinx_ise/xst.py b/src/tools/xilinx_ise/xst.py new file mode 100644 index 0000000..61c21ab --- /dev/null +++ b/src/tools/xilinx_ise/xst.py @@ -0,0 +1,101 @@ +from typing import Optional +from models.config import DIRECTORIES +from utils.source_resolver import expand_sources +from models.project import ProjectConfig +import subprocess +import os +import shutil + +def generate_xst_project_file(project: ProjectConfig, output_path: str): + """ + Generiert die XST .prj-Datei mit allen Quellcodes. + """ + with open(output_path, "w") as f: + # VHDL-Sources + for lib, src in expand_sources(project.sources.vhdl): + f.write(f"vhdl {lib} \"{DIRECTORIES.get_relative_prefix()}/{src}\"\n") + # Verilog-Sources + for lib, src in expand_sources(project.sources.verilog): + f.write(f"verilog {lib} \"{DIRECTORIES.get_relative_prefix()}/{src}\"\n") + + # Optionale Dependencies + if project.dependencies: + for dep in project.dependencies: + # Hier könnte man noch spezielle Sources aus dep.path expandieren + pass + +def generate_xst_script_file(project: ProjectConfig, output_path: str): + """ + Generiert die XST .scr-Datei mit den Synthese-Optionen. + """ + with open(output_path, "w") as f: + f.write(f"run ") + f.write(f"-ifn {project.name}.prj ") + f.write(f"-ofn {project.name}.ngc ") + f.write(f"-ifmt mixed ") + + if project.tool_options and project.tool_options.xst: + for opt in project.tool_options.xst: + f.write(f"{opt} ") + + f.write(f"-top {project.topmodule} ") + f.write(f"-ofmt NGC ") + f.write(f"-p {project.target_device} ") + + + +def run_xst(project: ProjectConfig, working_dir: Optional[str] = None): + """ + Führt Xilinx XST Synthese aus, basierend auf dem gegebenen Projekt. + + Args: + project (ProjectConfig): Geladene Projektkonfiguration. + working_dir (str, optional): Pfad, wo .prj/.scr liegen und gebaut werden soll. + Wenn None, wird das aktuelle Arbeitsverzeichnis verwendet. + """ + if working_dir is None: + working_dir = DIRECTORIES.build + + xilinx_bin_dir = os.path.join(project.xilinx_path, "bin", "lin64") # oder "nt64" für Windows + xst_executable = os.path.join(xilinx_bin_dir, "xst") + + if not os.path.exists(xst_executable): + raise FileNotFoundError(f"XST-Executable nicht gefunden unter: {xst_executable}") + + print(f"[hdlbuild] Starte XST Synthese über {xst_executable}") + print(f"[hdlbuild] Arbeitsverzeichnis: {working_dir}") + + cmd = [xst_executable] + + # Füge die "common" Optionen ein, wenn sie existieren + if project.tool_options and project.tool_options.common: + cmd.extend(project.tool_options.common) + + # Jetzt die XST-spezifischen Aufrufe + cmd.extend([ + "-ifn", f"{project.name}.scr" + ]) + + print(f"[hdlbuild] XST-Befehl: {' '.join(cmd)}") + + subprocess.run(cmd, cwd=working_dir, check=True) + +def copy_synthesis_report(project: ProjectConfig): + """ + Kopiert den Synthesebericht (.srp) vom Build-Verzeichnis ins Report-Verzeichnis + und benennt ihn sinnvoll um. + + Args: + project (ProjectConfig): Geladene Projektkonfiguration. + """ + src_path = os.path.join(DIRECTORIES.build, f"{project.name}.srp") + dst_path = os.path.join(DIRECTORIES.report, f"{project.name}.SynthesisReport") + + if not os.path.exists(src_path): + raise FileNotFoundError(f"Synthesebericht nicht gefunden: {src_path}") + + # Stelle sicher, dass das Zielverzeichnis existiert + os.makedirs(DIRECTORIES.report, exist_ok=True) + + shutil.copyfile(src_path, dst_path) + print(f"[hdlbuild] Synthesebericht kopiert nach {dst_path}") \ No newline at end of file diff --git a/src/utils/directory_manager.py b/src/utils/directory_manager.py index 8800c0f..6b9c73f 100644 --- a/src/utils/directory_manager.py +++ b/src/utils/directory_manager.py @@ -1,6 +1,6 @@ import os import shutil -from config import DIRECTORIES +from models.config import DIRECTORIES def ensure_directories_exist(): """