diff --git a/src/gui/main_window.py b/src/gui/main_window.py index ece51bf..aa59903 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -16,6 +16,7 @@ import customtkinter as ctk from config_manager import Config from vitis_checker import check_vitis, get_tool_status_dict +from zynq_checker import check_zynq_jtag, get_zynq_status_dict from flash_programmer import full_flash_program, FlashResult from bitstream_programmer import full_bitstream_program, BitstreamResult from ip_verifier import ping_ip @@ -75,6 +76,8 @@ class MainWindow(ctk.CTk): self._worker_thread: threading.Thread | None = None self._uart_available: bool = False self._uart_message: str = "" + self._zynq_jtag_present: bool = False + self._zynq_jtag_info = None self._load_config() self._build_ui() @@ -185,6 +188,13 @@ class MainWindow(ctk.CTk): ) self._ip_status.grid(row=0, column=2, sticky="e", padx=PADDING) + self._jtag_status = ctk.CTkLabel( + header, + text="JTAG: Checking...", + font=FONT_BODY, + ) + self._jtag_status.grid(row=0, column=3, sticky="e", padx=PADDING) + def _build_workflow_panel(self, parent: ctk.CTkFrame) -> None: """Build the step-by-step workflow panel.""" panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS) @@ -510,6 +520,22 @@ class MainWindow(ctk.CTk): self._ip_status.configure(text=f"IP: {ip} ✗", text_color=DANGER_COLOR) return False + # Check Zynq JTAG presence + self._log_message(" Checking Zynq JTAG presence...") + jtag_result = check_zynq_jtag(self._config, self._jtag_callback) + jtag_dict = get_zynq_status_dict(jtag_result) + if jtag_result.is_present: + self._zynq_jtag_present = True + self._zynq_jtag_info = jtag_result.devices[0] + part = jtag_result.devices[0].part_number + self._log_message(f" ✓ Zynq {part} detected on JTAG chain") + self._jtag_status.configure(text=f"JTAG: {part} ✓", text_color=SUCCESS_COLOR) + else: + self._zynq_jtag_present = False + self._zynq_jtag_info = None + self._log_message(f" ✗ Zynq not detected on JTAG: {jtag_result.error}") + self._jtag_status.configure(text="JTAG: Not detected", text_color=WARNING_COLOR) + # Check UART availability self._log_message(" Checking UART serial port...") port = self._config.serial_port @@ -568,6 +594,10 @@ class MainWindow(ctk.CTk): """Callback for flash programming progress.""" self._log_message(f" [{status}] {message}") + def _jtag_callback(self, status: str, message: str) -> None: + """Callback for JTAG scan progress.""" + self._log_message(f" [{status}] {message}") + def _run_step_3_load_bootloader(self) -> bool: """Step 3: Load bootloader (BIT + ELF) and verify.""" if not self._config: diff --git a/src/zynq_checker.py b/src/zynq_checker.py new file mode 100644 index 0000000..f7795d0 --- /dev/null +++ b/src/zynq_checker.py @@ -0,0 +1,297 @@ +"""Zynq JTAG presence checker for Zynq Flasher GUI. + +Connects to hw_server via Vivado and scans the JTAG chain to detect +Zynq devices, returning chip model and presence information. +""" + +from __future__ import annotations + +import os +import re +import shutil +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Callable + +from config_manager import Config + + +@dataclass +class ZynqDevice: + """Information about a detected Zynq device on the JTAG chain.""" + + name: str # Device name from JTAG chain (e.g. "xc7z100_1") + part_number: str # Extracted part number (e.g. "XC7Z100") + family: str # Family (e.g. "zynq7") + speed: str # Speed grade (e.g. "1", "2", "-1", "-2") + idcode: str = "" # JTAG IDCODE if available + + +@dataclass +class ZynqCheckResult: + """Result of Zynq device detection on the JTAG chain.""" + + devices: list[ZynqDevice] # All detected Zynq devices + is_present: bool # Whether any Zynq device was found + hw_server_url: str # hw_server connection URL used + error: str = "" # Error message if detection failed + + +# Zynq part number patterns from JTAG device names +# Examples: xc7z010_1, xc7z020_1, xc7z100_1, xc7z030_1 +ZYNQ_PART_PATTERN = re.compile( + r"^(xc\d+z\d+[a-z]?\d+)_\d+$", re.IGNORECASE +) + +# hw_server default port +HW_SERVER_PORT = 3121 + + +def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqCheckResult: + """Check for Zynq devices on the JTAG chain via hw_server. + + Connects to hw_server using Vivado batch mode, scans the JTAG chain, + and identifies Zynq devices by their JTAG device names. + + Args: + config: Application configuration. + callback: Optional progress callback(status, message). + + Returns: + ZynqCheckResult with detected devices and status. + """ + hw_server_url = f"localhost:{HW_SERVER_PORT}" + + if callback: + callback("start", "Connecting to hw_server...") + + # Find vivado executable + vivado_path = _get_vivado_path(config.vitis_path) + if not vivado_path: + return ZynqCheckResult( + devices=[], + is_present=False, + hw_server_url=hw_server_url, + error="Vivado not found", + ) + + # Create temporary TCL script + tcl_content = _build_jtag_query_tcl() + tcl_path = Path("/tmp/zynq_jtag_check_{}.tcl".format(os.getpid())) + try: + tcl_path.write_text(tcl_content) + + if callback: + callback("progress", "Scanning JTAG chain...") + + # Run Vivado in batch mode + cmd = [vivado_path, "-mode", "batch", "-source", str(tcl_path)] + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30, + ) + + output = result.stdout + result.stderr + + # Parse JTAG device information from output + devices = _parse_jtag_devices(output) + + if callback: + if devices: + callback("complete", f"Found {len(devices)} Zynq device(s)") + else: + callback("error", "No Zynq device found on JTAG chain") + + return ZynqCheckResult( + devices=devices, + is_present=len(devices) > 0, + hw_server_url=hw_server_url, + error="" if devices else "No Zynq device found on JTAG chain", + ) + + except subprocess.TimeoutExpired: + return ZynqCheckResult( + devices=[], + is_present=False, + hw_server_url=hw_server_url, + error="JTAG scan timed out after 30s", + ) + except OSError as e: + return ZynqCheckResult( + devices=[], + is_present=False, + hw_server_url=hw_server_url, + error=f"OS error: {e}", + ) + finally: + tcl_path.unlink(missing_ok=True) + + +def _build_jtag_query_tcl() -> str: + """Build TCL script for JTAG device query. + + Returns: + TCL script content as string. + """ + return """ +open_hw_manager +connect_hw_server +open_hw_target +puts "===JTAG_START===" +foreach dev [get_hw_devices] { + set name [get_property NAME $dev] + puts "DEVICE:$name" +} +puts "===JTAG_END===" +quit +""" + + +def _parse_jtag_devices(output: str) -> list[ZynqDevice]: + """Parse JTAG device information from Vivado output. + + Args: + output: Combined stdout/stderr from Vivado batch execution. + + Returns: + List of ZynqDevice objects for detected Zynq chips. + """ + devices: list[ZynqDevice] = [] + + # Extract device list from output + devices_section = _extract_section(output, "===JTAG_START===", "===JTAG_END===") + if not devices_section: + return devices + + for line in devices_section.splitlines(): + line = line.strip() + if line.startswith("DEVICE:"): + device_name = line[len("DEVICE:"):] + device = _parse_zynq_device(device_name) + if device: + devices.append(device) + + return devices + + +def _extract_section( + text: str, start_marker: str, end_marker: str +) -> str | None: + """Extract text between two markers, handling duplicate markers. + + Finds the last occurrence of each marker to avoid matching + markers inside TCL script source code. + + Args: + text: Full output text. + start_marker: Starting marker string. + end_marker: Ending marker string. + + Returns: + Text between markers, or None if not found. + """ + # Find last occurrence of each marker to avoid matching + # markers inside TCL script source code + start_idx = text.rfind(start_marker) + end_idx = text.rfind(end_marker) + if start_idx == -1 or end_idx == -1 or start_idx >= end_idx: + return None + return text[start_idx + len(start_marker):end_idx].strip() + + +def _parse_zynq_device(device_name: str) -> ZynqDevice | None: + """Parse a Zynq device from its JTAG chain name. + + Args: + device_name: Device name from JTAG chain (e.g. "xc7z100_1"). + + Returns: + ZynqDevice if it's a Zynq chip, None otherwise. + """ + match = ZYNQ_PART_PATTERN.match(device_name) + if not match: + return None + + part_base = match.group(1) + # Convert to uppercase for part number + part_number = part_base.upper() + + # Extract family and speed from part number + # Examples: XC7Z010 -> family=zynq7, speed=1; XC7Z100 -> family=zynq7, speed=1 + family = "zynq7" + speed = "1" # Default speed grade + + # Parse speed grade from the numeric suffix + speed_match = re.search(r"(\d+)$", part_base) + if speed_match: + speed_num = int(speed_match.group(1)) + # Map numeric speed to speed grade string + if speed_num >= 2: + speed = "-2" + elif speed_num >= 1: + speed = "-1" + + return ZynqDevice( + name=device_name, + part_number=part_number, + family=family, + speed=speed, + ) + + +def _get_vivado_path(vitis_path: str) -> str | None: + """Find the Vivado executable path. + + Args: + vitis_path: Vitis installation path. + + Returns: + Path to Vivado executable, or None if not found. + """ + path = shutil.which("vivado") + if path: + return path + if vitis_path: + candidate = Path(vitis_path) / "bin" / "vivado" + if candidate.exists(): + return str(candidate) + candidate = Path(vitis_path) / "bin" / "vivado.exe" + if candidate.exists(): + return str(candidate) + common_paths = [ + "/opt/xilinx/bin/vivado", + os.path.expanduser("~/Xilinx/Vivado/bin/vivado"), + ] + for p in common_paths: + if os.path.isfile(p): + return p + return None + + +def get_zynq_status_dict(result: ZynqCheckResult) -> dict: + """Convert a ZynqCheckResult to a serializable dictionary. + + Args: + result: ZynqCheckResult from check_zynq_jtag(). + + Returns: + Dictionary suitable for JSON serialization or GUI display. + """ + return { + "is_present": result.is_present, + "hw_server_url": result.hw_server_url, + "error": result.error, + "devices": [ + { + "name": dev.name, + "part_number": dev.part_number, + "family": dev.family, + "speed": dev.speed, + "idcode": dev.idcode, + } + for dev in result.devices + ], + }