feat: Xilinx Kit directory selector + multi-version scan

Configuration panel now has a 'Xilinx Kit:' row at the top with:
- Entry field showing current path
- [Browse] button using system directory dialog (cross-platform)
- Select root directory (e.g. /data/xilinx or C:/Xilinx)

vitis_checker.py now scans xilinx_root subdirectories:
  1. System PATH (shutil.which) — works on all platforms
  2. Legacy vitis_path/bin
  3. xilinx_root/Vitis/*/bin and Vivado/*/bin (all versions, newest first)
  4. Common install paths (Windows: C:/Xilinx, Linux: /data/xilinx, /opt/xilinx)

Multi-version: scans all version directories under Vitis/ and Vivado/,
picks the newest version for each tool.

Config: xilinx_path replaces vitis_path (backward compat kept)
This commit is contained in:
Jeremy Shen
2026-06-10 14:27:47 +08:00
parent 699ad82834
commit 93ab224d6d
7 changed files with 132 additions and 69 deletions
+1
View File
@@ -21,5 +21,6 @@ step_timeouts:
tftp_reboot: 120 tftp_reboot: 120
tftp_upload_name: z7bin tftp_upload_name: z7bin
uart_delay: 3 uart_delay: 3
xilinx_path: ""
vitis_path: '' vitis_path: ''
zynq_ip: 192.168.100.11 zynq_ip: 192.168.100.11
+4 -2
View File
@@ -1,8 +1,10 @@
# Default configuration for Zynq XC7Z100 Flasher # Default configuration for Zynq XC7Z100 Flasher
# Paths are relative to this config file's directory. # Paths are relative to this config file's directory.
# Vitis/Vivado installation path (where bootgen, impact, etc. reside) # Xilinx installation root (e.g., /data/xilinx or C:/Xilinx)
vitis_path: "" # The program scans subdirectories for Vitis/Vivado versions.
# If empty, tools are searched via system PATH.
xilinx_path: ""
# Zynq device network settings # Zynq device network settings
zynq_ip: "192.168.100.11" zynq_ip: "192.168.100.11"
+8 -2
View File
@@ -166,7 +166,10 @@ def program_bitstream(
ps7_tcl = _find_ps7_init_tcl(config) ps7_tcl = _find_ps7_init_tcl(config)
# Try xsct first (preferred) # Try xsct first (preferred)
xsct = get_xsct_path(config.vitis_path) xsct = get_xsct_path(
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
)
if xsct: if xsct:
timeout = config.step_timeouts.get("bitstream", 60) timeout = config.step_timeouts.get("bitstream", 60)
return _program_with_xsct(xsct, bit_path, callback, ps7_tcl, timeout) return _program_with_xsct(xsct, bit_path, callback, ps7_tcl, timeout)
@@ -340,7 +343,10 @@ def run_elf(
callback("start", f"Running ELF: {elf_path.name}...") callback("start", f"Running ELF: {elf_path.name}...")
# Use xsct for CPU operations (Vivado HM cannot program PS CPU) # Use xsct for CPU operations (Vivado HM cannot program PS CPU)
xsct = get_xsct_path(config.vitis_path) xsct = get_xsct_path(
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
)
if not xsct: if not xsct:
return BitstreamResult( return BitstreamResult(
step="elf", step="elf",
+3 -3
View File
@@ -15,7 +15,7 @@ import yaml
DEFAULT_CONFIG: dict[str, Any] = { DEFAULT_CONFIG: dict[str, Any] = {
"vitis_path": "", "xilinx_path": "",
"zynq_ip": "192.168.100.11", "zynq_ip": "192.168.100.11",
"tftp_upload_name": "z7bin", "tftp_upload_name": "z7bin",
"serial_port": "", "serial_port": "",
@@ -52,7 +52,7 @@ class Config:
directory and resolved to absolute paths when accessed. directory and resolved to absolute paths when accessed.
""" """
vitis_path: str = "" xilinx_path: str = ""
zynq_ip: str = "192.168.100.11" zynq_ip: str = "192.168.100.11"
tftp_upload_name: str = "z7bin" tftp_upload_name: str = "z7bin"
serial_port: str = "" serial_port: str = ""
@@ -160,7 +160,7 @@ class Config:
Dictionary representation of the config. Dictionary representation of the config.
""" """
data = { data = {
"vitis_path": self._relative_path(self.vitis_path), "xilinx_path": self.xilinx_path,
"zynq_ip": self.zynq_ip, "zynq_ip": self.zynq_ip,
"tftp_upload_name": self.tftp_upload_name, "tftp_upload_name": self.tftp_upload_name,
"serial_port": self.serial_port, "serial_port": self.serial_port,
+8 -2
View File
@@ -112,7 +112,10 @@ def wipe_flash(
Returns: Returns:
FlashResult with status. FlashResult with status.
""" """
flash_tool = get_program_flash_path(config.vitis_path) flash_tool = get_program_flash_path(
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
)
if not flash_tool: if not flash_tool:
return FlashResult( return FlashResult(
step="wipe", step="wipe",
@@ -225,7 +228,10 @@ def program_flash_bin(
message=f"BIN file not found: {bin_path}", message=f"BIN file not found: {bin_path}",
) )
flash_tool = get_program_flash_path(config.vitis_path) flash_tool = get_program_flash_path(
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
)
if not flash_tool: if not flash_tool:
return FlashResult( return FlashResult(
step="program", step="program",
+35 -8
View File
@@ -182,6 +182,8 @@ class MainWindow(ctk.CTk):
# Sync IP address # Sync IP address
if self._ip_string_var: if self._ip_string_var:
self._config.zynq_ip = self._ip_string_var.get() self._config.zynq_ip = self._ip_string_var.get()
# Sync Xilinx Kit path
self._config.xilinx_path = self._xilinx_path_var.get()
# Sync serial port # Sync serial port
self._config.serial_port = self._port_var.get() self._config.serial_port = self._port_var.get()
# Sync file paths from FileSelectors # Sync file paths from FileSelectors
@@ -219,6 +221,17 @@ class MainWindow(ctk.CTk):
if self._config: if self._config:
self._config.erase_all = self._erase_cb_var.get() self._config.erase_all = self._erase_cb_var.get()
def _browse_xilinx_path(self) -> None:
"""Open directory dialog to select Xilinx root folder."""
from tkinter import filedialog
path = filedialog.askdirectory(title="Select Xilinx Root Directory")
if path:
self._xilinx_path_var.set(path)
if self._config:
self._config.xilinx_path = path
# Re-check tools with new path
self._check_vitis()
# ── UI Construction ──────────────────────────────────────── # ── UI Construction ────────────────────────────────────────
def _build_ui(self) -> None: def _build_ui(self) -> None:
@@ -418,7 +431,7 @@ class MainWindow(ctk.CTk):
row=0, column=0, sticky="nsew", row=0, column=0, sticky="nsew",
padx=PADDING, pady=PADDING, padx=PADDING, pady=PADDING,
) )
panel.grid_rowconfigure(7, weight=1) panel.grid_rowconfigure(8, weight=1)
title = ctk.CTkLabel( title = ctk.CTkLabel(
panel, panel,
@@ -427,9 +440,23 @@ class MainWindow(ctk.CTk):
) )
title.grid(row=0, column=0, padx=PADDING, pady=PADDING) title.grid(row=0, column=0, padx=PADDING, pady=PADDING)
# ── Xilinx Kit path ──
xilinx_frame = ctk.CTkFrame(panel)
xilinx_frame.grid(row=1, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
xilinx_frame.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(xilinx_frame, text="Xilinx Kit:", font=FONT_BODY).grid(row=0, column=0, sticky="w")
self._xilinx_path_var = ctk.StringVar(value=self._config.xilinx_path)
self._xilinx_entry = ctk.CTkEntry(xilinx_frame, textvariable=self._xilinx_path_var)
self._xilinx_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
ctk.CTkButton(
xilinx_frame, text="Browse", font=FONT_SMALL, width=70,
command=self._browse_xilinx_path,
).grid(row=0, column=2, padx=(0, PADDING_SMALL))
# IP address # IP address
ip_frame = ctk.CTkFrame(panel) ip_frame = ctk.CTkFrame(panel)
ip_frame.grid(row=1, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) ip_frame.grid(row=2, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
ip_frame.grid_columnconfigure(1, weight=1) ip_frame.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(ip_frame, text="Zynq IP:", font=FONT_BODY).grid(row=0, column=0, sticky="w") ctk.CTkLabel(ip_frame, text="Zynq IP:", font=FONT_BODY).grid(row=0, column=0, sticky="w")
@@ -446,7 +473,7 @@ class MainWindow(ctk.CTk):
if self._config.bootloader_bit_path: if self._config.bootloader_bit_path:
resolved = self._config.resolve_path(self._config.bootloader_bit_path) resolved = self._config.resolve_path(self._config.bootloader_bit_path)
self._bit_selector.set_path(str(resolved)) self._bit_selector.set_path(str(resolved))
self._bit_selector.grid(row=2, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) self._bit_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
# ELF path (Bootloader) # ELF path (Bootloader)
self._elf_selector = FileSelector( self._elf_selector = FileSelector(
@@ -457,7 +484,7 @@ class MainWindow(ctk.CTk):
if self._config.bootloader_elf_path: if self._config.bootloader_elf_path:
resolved = self._config.resolve_path(self._config.bootloader_elf_path) resolved = self._config.resolve_path(self._config.bootloader_elf_path)
self._elf_selector.set_path(str(resolved)) self._elf_selector.set_path(str(resolved))
self._elf_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) self._elf_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
# Bootloader BIN path (Flash programming) # Bootloader BIN path (Flash programming)
self._bootloader_bin_selector = FileSelector( self._bootloader_bin_selector = FileSelector(
@@ -468,7 +495,7 @@ class MainWindow(ctk.CTk):
if self._config.bootloader_bin_path: if self._config.bootloader_bin_path:
resolved = self._config.resolve_path(self._config.bootloader_bin_path) resolved = self._config.resolve_path(self._config.bootloader_bin_path)
self._bootloader_bin_selector.set_path(str(resolved)) self._bootloader_bin_selector.set_path(str(resolved))
self._bootloader_bin_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) self._bootloader_bin_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
# FSBL ELF path (required by program_flash for QSPI programming) # FSBL ELF path (required by program_flash for QSPI programming)
self._fsbl_selector = FileSelector( self._fsbl_selector = FileSelector(
@@ -479,7 +506,7 @@ class MainWindow(ctk.CTk):
if self._config.fsbl_elf_path: if self._config.fsbl_elf_path:
resolved = self._config.resolve_path(self._config.fsbl_elf_path) resolved = self._config.resolve_path(self._config.fsbl_elf_path)
self._fsbl_selector.set_path(str(resolved)) self._fsbl_selector.set_path(str(resolved))
self._fsbl_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) self._fsbl_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
# Flash options # Flash options
flash_frame = ctk.CTkFrame(panel) flash_frame = ctk.CTkFrame(panel)
@@ -512,7 +539,7 @@ class MainWindow(ctk.CTk):
if self._config.firmware_bin_path: if self._config.firmware_bin_path:
resolved = self._config.resolve_path(self._config.firmware_bin_path) resolved = self._config.resolve_path(self._config.firmware_bin_path)
self._firmware_bin_selector.set_path(str(resolved)) self._firmware_bin_selector.set_path(str(resolved))
self._firmware_bin_selector.grid(row=7, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) self._firmware_bin_selector.grid(row=8, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
# Save button # Save button
save_btn = ctk.CTkButton( save_btn = ctk.CTkButton(
@@ -521,7 +548,7 @@ class MainWindow(ctk.CTk):
font=FONT_SMALL, font=FONT_SMALL,
command=self._save_config, command=self._save_config,
) )
save_btn.grid(row=8, column=0, padx=PADDING, pady=PADDING) save_btn.grid(row=9, column=0, padx=PADDING, pady=PADDING)
def _build_serial_panel(self, parent: ctk.CTkFrame) -> None: def _build_serial_panel(self, parent: ctk.CTkFrame) -> None:
"""Build the serial monitor panel.""" """Build the serial monitor panel."""
+73 -52
View File
@@ -52,86 +52,117 @@ TOOL_ALIASES: dict[str, list[str]] = {
} }
def _find_executable(exec_name: str, vitis_path: str, system: str) -> str | None: def _find_executable(
"""Find an executable by name using PATH and Vitis search paths. exec_name: str,
vitis_path: str,
system: str,
xilinx_root: str = "",
) -> str | None:
"""Find an executable by name using PATH, Vitis, and Xilinx root.
Search order:
1. System PATH (shutil.which)
2. vitis_path/bin (legacy config)
3. xilinx_root/Vitis/*/bin and Vivado/*/bin (multi-version scan)
4. Common install paths
Args: Args:
exec_name: Executable name (without extension). exec_name: Executable name (without extension).
vitis_path: Vitis/Vivado installation root directory. vitis_path: Legacy Vitis installation directory.
system: Platform identifier (linux, windows, darwin). system: Platform identifier (linux, windows, darwin).
xilinx_root: Xilinx root directory (e.g., /data/xilinx or C:/Xilinx).
Returns: Returns:
Absolute path to the executable, or None. Absolute path to the executable, or None.
""" """
exec_suffix = ".exe" if system == "windows" else "" exec_suffix = ".exe" if system == "windows" else ""
# 1. shutil.which() — most reliable cross-platform PATH resolution # 1. System PATH
found = shutil.which(f"{exec_name}{exec_suffix}") found = shutil.which(f"{exec_name}{exec_suffix}")
if found: if found:
return found return found
# 2. Scan vitis_path/bin # 2. Legacy vitis_path/bin
if vitis_path: if vitis_path:
candidate = Path(vitis_path) / "bin" / f"{exec_name}{exec_suffix}" candidate = Path(vitis_path) / "bin" / f"{exec_name}{exec_suffix}"
if candidate.is_file(): if candidate.is_file():
return str(candidate) return str(candidate)
# 3. Common Xilinx install paths # 3. Scan xilinx_root for all versions (Vitis + Vivado)
common_paths = [ if xilinx_root:
f"/data/xilinx/Vitis/2023.2/bin/{exec_name}", xr = Path(xilinx_root)
f"/data/xilinx/Vivado/2023.2/bin/{exec_name}", for product in ("Vitis", "Vivado"):
f"/opt/xilinx/bin/{exec_name}", pdir = xr / product
os.path.expanduser(f"~/Xilinx/Vitis/2023.2/bin/{exec_name}"), if not pdir.is_dir():
os.path.expanduser(f"~/Xilinx/Vivado/2023.2/bin/{exec_name}"), continue
] # Scan version directories, newest first
for p in common_paths: versions = sorted(
[d for d in pdir.iterdir() if d.is_dir()],
reverse=True,
)
for ver_dir in versions:
candidate = ver_dir / "bin" / f"{exec_name}{exec_suffix}"
if candidate.is_file():
return str(candidate)
# 4. Common install paths (cross-platform)
if system == "windows":
common = [
f"C:/Xilinx/Vitis/2023.2/bin/{exec_name}.exe",
f"C:/Xilinx/Vivado/2023.2/bin/{exec_name}.exe",
]
else:
common = [
f"/data/xilinx/Vitis/2023.2/bin/{exec_name}",
f"/data/xilinx/Vivado/2023.2/bin/{exec_name}",
f"/opt/xilinx/bin/{exec_name}",
os.path.expanduser(f"~/Xilinx/Vitis/2023.2/bin/{exec_name}"),
os.path.expanduser(f"~/Xilinx/Vivado/2023.2/bin/{exec_name}"),
]
for p in common:
if os.path.isfile(p): if os.path.isfile(p):
return p return p
return None return None
def get_xsct_path(vitis_path: str = "") -> str | None: def get_xsct_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
"""Find the xsct (Xilinx Software Command Line Tool) executable. """Find the xsct (Xilinx Software Command Line Tool) executable.
xsct is the primary JTAG scripting tool for Vivado/Vitis (2015.x+).
Args: Args:
vitis_path: Optional Vitis installation directory. vitis_path: Legacy Vitis install path.
xilinx_root: Xilinx root directory.
Returns: Returns:
Absolute path to xsct, or None. Absolute path to xsct, or None.
""" """
return _find_executable("xsct", vitis_path, platform.system().lower()) return _find_executable("xsct", vitis_path, platform.system().lower(), xilinx_root)
def get_program_flash_path(vitis_path: str = "") -> str | None: def get_program_flash_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
"""Find the program_flash executable (QSPI flash programming). """Find the program_flash executable.
program_flash is part of Vitis/SDK (2014.x+).
Args: Args:
vitis_path: Optional Vitis installation directory. vitis_path: Legacy Vitis install path.
xilinx_root: Xilinx root directory.
Returns: Returns:
Absolute path to program_flash, or None. Absolute path to program_flash, or None.
""" """
return _find_executable("program_flash", vitis_path, platform.system().lower()) return _find_executable("program_flash", vitis_path, platform.system().lower(), xilinx_root)
def get_bootgen_path(vitis_path: str = "") -> str | None: def get_bootgen_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
"""Find the bootgen executable (BOOT.BIN generation). """Find the bootgen executable.
bootgen creates boot images from BIF descriptions. It does NOT
program flash — use program_flash for that.
Args: Args:
vitis_path: Optional Vitis installation directory. vitis_path: Legacy Vitis install path.
xilinx_root: Xilinx root directory.
Returns: Returns:
Absolute path to bootgen, or None. Absolute path to bootgen, or None.
""" """
return _find_executable("bootgen", vitis_path, platform.system().lower()) return _find_executable("bootgen", vitis_path, platform.system().lower(), xilinx_root)
def _get_tool_version(tool_path: str, tool_name: str) -> str: def _get_tool_version(tool_path: str, tool_name: str) -> str:
@@ -162,8 +193,6 @@ def _get_tool_version(tool_path: str, tool_name: str) -> str:
def check_vitis(config: Config) -> VitisCheckResult: def check_vitis(config: Config) -> VitisCheckResult:
"""Check availability of all required Xilinx tools. """Check availability of all required Xilinx tools.
Scans PATH and Vitis paths for: xsct, program_flash, bootgen.
Args: Args:
config: Application configuration. config: Application configuration.
@@ -171,19 +200,20 @@ def check_vitis(config: Config) -> VitisCheckResult:
VitisCheckResult with per-tool status. VitisCheckResult with per-tool status.
""" """
system = platform.system().lower() system = platform.system().lower()
vitis_path = config.vitis_path or "" vitis_path = config.vitis_path if hasattr(config, 'vitis_path') else ""
xilinx_root = config.xilinx_path if hasattr(config, 'xilinx_path') else ""
tools: dict[str, ToolStatus] = {} tools: dict[str, ToolStatus] = {}
errors: list[str] = [] errors: list[str] = []
for tool_name, aliases in TOOL_ALIASES.items(): for tool_name, aliases in TOOL_ALIASES.items():
status = _check_tool(tool_name, aliases, vitis_path, system) status = _check_tool(tool_name, aliases, vitis_path, system, xilinx_root)
tools[tool_name] = status tools[tool_name] = status
if not status.found: if not status.found:
errors.append(f"{tool_name}: {status.error or 'not found'}") errors.append(f"{tool_name}: {status.error or 'not found'}")
return VitisCheckResult( return VitisCheckResult(
system=system, system=system,
vitis_path=vitis_path, vitis_path=xilinx_root or vitis_path,
tools=tools, tools=tools,
is_ready=len(errors) == 0, is_ready=len(errors) == 0,
errors=errors, errors=errors,
@@ -195,20 +225,11 @@ def _check_tool(
aliases: list[str], aliases: list[str],
vitis_path: str, vitis_path: str,
system: str, system: str,
xilinx_root: str = "",
) -> ToolStatus: ) -> ToolStatus:
"""Check if a single tool is available. """Check if a single tool is available."""
Args:
tool_name: Canonical tool name.
aliases: Executable names to try.
vitis_path: Vitis installation path.
system: Platform identifier.
Returns:
ToolStatus with detection results.
"""
for alias in aliases: for alias in aliases:
found = _find_executable(alias, vitis_path, system) found = _find_executable(alias, vitis_path, system, xilinx_root)
if found: if found:
version = _get_tool_version(found, tool_name) version = _get_tool_version(found, tool_name)
return ToolStatus( return ToolStatus(
@@ -223,9 +244,9 @@ def _check_tool(
name=tool_name, name=tool_name,
found=False, found=False,
error=( error=(
f"Could not locate {', '.join(aliases)} in PATH or {vitis_path}/bin" f"Could not locate {', '.join(aliases)} in PATH"
if vitis_path + (f" or {xilinx_root}" if xilinx_root else "")
else f"Could not locate {', '.join(aliases)} in PATH" + (f" or {vitis_path}" if vitis_path else "")
), ),
) )