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.
This commit is contained in:
2025-04-26 13:47:50 +00:00
parent 7a03040926
commit 91f4f03d97
13 changed files with 493 additions and 18 deletions

3
.gitignore vendored
View File

@@ -183,4 +183,5 @@ output/
vhdl/
poetry.lock
project.yml
.project/
.project/
.devcontainer/

View File

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

View File

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

View File

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

26
src/models/config.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

101
src/tools/xilinx_ise/xst.py Normal file
View File

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

View File

@@ -1,6 +1,6 @@
import os
import shutil
from config import DIRECTORIES
from models.config import DIRECTORIES
def ensure_directories_exist():
"""