From 93ab224d6d3c04e6a20acfcce281ae690689f1fd Mon Sep 17 00:00:00 2001 From: Jeremy Shen Date: Wed, 10 Jun 2026 14:27:47 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Xilinx=20Kit=20directory=20?= =?UTF-8?q?selector=20+=20multi-version=20scan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- config.yaml | 1 + config/default_config.yaml | 6 +- src/bitstream_programmer.py | 10 ++- src/config_manager.py | 6 +- src/flash_programmer.py | 10 ++- src/gui/main_window.py | 43 ++++++++++--- src/vitis_checker.py | 125 +++++++++++++++++++++--------------- 7 files changed, 132 insertions(+), 69 deletions(-) diff --git a/config.yaml b/config.yaml index aafc114..2bff602 100644 --- a/config.yaml +++ b/config.yaml @@ -21,5 +21,6 @@ step_timeouts: tftp_reboot: 120 tftp_upload_name: z7bin uart_delay: 3 +xilinx_path: "" vitis_path: '' zynq_ip: 192.168.100.11 diff --git a/config/default_config.yaml b/config/default_config.yaml index 61330aa..6303bcc 100644 --- a/config/default_config.yaml +++ b/config/default_config.yaml @@ -1,8 +1,10 @@ # Default configuration for Zynq XC7Z100 Flasher # Paths are relative to this config file's directory. -# Vitis/Vivado installation path (where bootgen, impact, etc. reside) -vitis_path: "" +# Xilinx installation root (e.g., /data/xilinx or C:/Xilinx) +# The program scans subdirectories for Vitis/Vivado versions. +# If empty, tools are searched via system PATH. +xilinx_path: "" # Zynq device network settings zynq_ip: "192.168.100.11" diff --git a/src/bitstream_programmer.py b/src/bitstream_programmer.py index e8272d5..16bc941 100644 --- a/src/bitstream_programmer.py +++ b/src/bitstream_programmer.py @@ -166,7 +166,10 @@ def program_bitstream( ps7_tcl = _find_ps7_init_tcl(config) # 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: timeout = config.step_timeouts.get("bitstream", 60) 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}...") # 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: return BitstreamResult( step="elf", diff --git a/src/config_manager.py b/src/config_manager.py index e8267b4..fd2c5c9 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -15,7 +15,7 @@ import yaml DEFAULT_CONFIG: dict[str, Any] = { - "vitis_path": "", + "xilinx_path": "", "zynq_ip": "192.168.100.11", "tftp_upload_name": "z7bin", "serial_port": "", @@ -52,7 +52,7 @@ class Config: directory and resolved to absolute paths when accessed. """ - vitis_path: str = "" + xilinx_path: str = "" zynq_ip: str = "192.168.100.11" tftp_upload_name: str = "z7bin" serial_port: str = "" @@ -160,7 +160,7 @@ class Config: Dictionary representation of the config. """ data = { - "vitis_path": self._relative_path(self.vitis_path), + "xilinx_path": self.xilinx_path, "zynq_ip": self.zynq_ip, "tftp_upload_name": self.tftp_upload_name, "serial_port": self.serial_port, diff --git a/src/flash_programmer.py b/src/flash_programmer.py index c807411..455f304 100644 --- a/src/flash_programmer.py +++ b/src/flash_programmer.py @@ -112,7 +112,10 @@ def wipe_flash( Returns: 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: return FlashResult( step="wipe", @@ -225,7 +228,10 @@ def program_flash_bin( 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: return FlashResult( step="program", diff --git a/src/gui/main_window.py b/src/gui/main_window.py index 310a465..d5470a4 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -182,6 +182,8 @@ class MainWindow(ctk.CTk): # Sync IP address if self._ip_string_var: 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 self._config.serial_port = self._port_var.get() # Sync file paths from FileSelectors @@ -219,6 +221,17 @@ class MainWindow(ctk.CTk): if self._config: 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 ──────────────────────────────────────── def _build_ui(self) -> None: @@ -418,7 +431,7 @@ class MainWindow(ctk.CTk): row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING, ) - panel.grid_rowconfigure(7, weight=1) + panel.grid_rowconfigure(8, weight=1) title = ctk.CTkLabel( panel, @@ -427,9 +440,23 @@ class MainWindow(ctk.CTk): ) 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_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) 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: resolved = self._config.resolve_path(self._config.bootloader_bit_path) 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) self._elf_selector = FileSelector( @@ -457,7 +484,7 @@ class MainWindow(ctk.CTk): if 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.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) self._bootloader_bin_selector = FileSelector( @@ -468,7 +495,7 @@ class MainWindow(ctk.CTk): if 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.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) self._fsbl_selector = FileSelector( @@ -479,7 +506,7 @@ class MainWindow(ctk.CTk): if 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.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_frame = ctk.CTkFrame(panel) @@ -512,7 +539,7 @@ class MainWindow(ctk.CTk): if 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.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_btn = ctk.CTkButton( @@ -521,7 +548,7 @@ class MainWindow(ctk.CTk): font=FONT_SMALL, 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: """Build the serial monitor panel.""" diff --git a/src/vitis_checker.py b/src/vitis_checker.py index b701da8..b1e9ba0 100644 --- a/src/vitis_checker.py +++ b/src/vitis_checker.py @@ -52,86 +52,117 @@ TOOL_ALIASES: dict[str, list[str]] = { } -def _find_executable(exec_name: str, vitis_path: str, system: str) -> str | None: - """Find an executable by name using PATH and Vitis search paths. +def _find_executable( + 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: 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). + xilinx_root: Xilinx root directory (e.g., /data/xilinx or C:/Xilinx). Returns: Absolute path to the executable, or None. """ 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}") if found: return found - # 2. Scan vitis_path/bin + # 2. Legacy vitis_path/bin if vitis_path: candidate = Path(vitis_path) / "bin" / f"{exec_name}{exec_suffix}" if candidate.is_file(): return str(candidate) - # 3. Common Xilinx install paths - common_paths = [ - 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_paths: + # 3. Scan xilinx_root for all versions (Vitis + Vivado) + if xilinx_root: + xr = Path(xilinx_root) + for product in ("Vitis", "Vivado"): + pdir = xr / product + if not pdir.is_dir(): + continue + # Scan version directories, newest first + 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): return p 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. - xsct is the primary JTAG scripting tool for Vivado/Vitis (2015.x+). - Args: - vitis_path: Optional Vitis installation directory. + vitis_path: Legacy Vitis install path. + xilinx_root: Xilinx root directory. Returns: 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: - """Find the program_flash executable (QSPI flash programming). - - program_flash is part of Vitis/SDK (2014.x+). +def get_program_flash_path(vitis_path: str = "", xilinx_root: str = "") -> str | None: + """Find the program_flash executable. Args: - vitis_path: Optional Vitis installation directory. + vitis_path: Legacy Vitis install path. + xilinx_root: Xilinx root directory. Returns: 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: - """Find the bootgen executable (BOOT.BIN generation). - - bootgen creates boot images from BIF descriptions. It does NOT - program flash — use program_flash for that. +def get_bootgen_path(vitis_path: str = "", xilinx_root: str = "") -> str | None: + """Find the bootgen executable. Args: - vitis_path: Optional Vitis installation directory. + vitis_path: Legacy Vitis install path. + xilinx_root: Xilinx root directory. Returns: 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: @@ -162,8 +193,6 @@ def _get_tool_version(tool_path: str, tool_name: str) -> str: def check_vitis(config: Config) -> VitisCheckResult: """Check availability of all required Xilinx tools. - Scans PATH and Vitis paths for: xsct, program_flash, bootgen. - Args: config: Application configuration. @@ -171,19 +200,20 @@ def check_vitis(config: Config) -> VitisCheckResult: VitisCheckResult with per-tool status. """ 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] = {} errors: list[str] = [] 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 if not status.found: errors.append(f"{tool_name}: {status.error or 'not found'}") return VitisCheckResult( system=system, - vitis_path=vitis_path, + vitis_path=xilinx_root or vitis_path, tools=tools, is_ready=len(errors) == 0, errors=errors, @@ -195,20 +225,11 @@ def _check_tool( aliases: list[str], vitis_path: str, system: str, + xilinx_root: str = "", ) -> ToolStatus: - """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. - """ + """Check if a single tool is available.""" for alias in aliases: - found = _find_executable(alias, vitis_path, system) + found = _find_executable(alias, vitis_path, system, xilinx_root) if found: version = _get_tool_version(found, tool_name) return ToolStatus( @@ -223,9 +244,9 @@ def _check_tool( name=tool_name, found=False, error=( - f"Could not locate {', '.join(aliases)} in PATH or {vitis_path}/bin" - if vitis_path - else f"Could not locate {', '.join(aliases)} in PATH" + f"Could not locate {', '.join(aliases)} in PATH" + + (f" or {xilinx_root}" if xilinx_root else "") + + (f" or {vitis_path}" if vitis_path else "") ), )