From 474e3f122b655940607b4b671881ba1f4229bfcc Mon Sep 17 00:00:00 2001 From: Jeremy Shen Date: Thu, 11 Jun 2026 17:02:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20use=20Vivado=20TCL=20for=20JTAG=20check?= =?UTF-8?q?=20=E2=80=94=20xsct=20'targets'=20has=20broken=20TCL=20list=20p?= =?UTF-8?q?arsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xsct's [targets] returns unquoted TCL lists where multi-word names (e.g. 'ARM Cortex-A9 MPCore #0') are split into individual words by foreach, making it impossible to reconstruct device names. Vivado TCL with open_hw_manager works reliably: - Cold start (no hw_server): 7.1s on Linux - hw_server already running: 6.8s on Linux - Provides IDCODE (converted from Vivado binary to hex), device names Smart hw_server lifecycle: - Check port 3121 before starting — reuse if already running - Start hw_server only when needed (poll port up to 3s) - Don't kill hw_server that we didn't start Output format parsed: DEVICE:arm_dap_0 IDCODE:010010111010...0111 → arm_dap_0 idcode=4BA00477 PS detected → xc7z100_1 idcode=03736093 PL detected → XC7Z100 Tested on Linux 2023.2, cold + warm start, confirmed working. --- src/zynq_checker.py | 302 ++++++++++++++++++++++---------------------- 1 file changed, 150 insertions(+), 152 deletions(-) diff --git a/src/zynq_checker.py b/src/zynq_checker.py index bb9b565..d258416 100644 --- a/src/zynq_checker.py +++ b/src/zynq_checker.py @@ -1,27 +1,30 @@ """Zynq JTAG presence checker for Zynq Flasher GUI. -Connects to hw_server via xsct and scans the JTAG chain to detect -Zynq devices, returning chip model, IDCODE, PS/PL presence. +Uses Vivado batch-mode TCL to scan the JTAG chain via hw_server, +returning device names, IDCODEs, and PS/PL presence. """ from __future__ import annotations import re +import socket import subprocess +import tempfile import time from dataclasses import dataclass, field +from pathlib import Path from typing import Callable from config_manager import Config -from vitis_checker import get_xsct_path, get_hw_server_path, build_tool_command +from vitis_checker import get_vivado_path, get_hw_server_path, build_tool_command @dataclass class JtagDevice: """Information about a device on the JTAG chain.""" - name: str # Device name (e.g. "xc7z100", "arm_dap") - idcode: str = "" # JTAG IDCODE hex string + name: str + idcode: str = "" # IDCODE in hex (e.g. "03736093") is_zynq: bool = False is_arm_dap: bool = False is_pl_device: bool = False @@ -32,9 +35,9 @@ class ZynqDevice: """Information about a detected Zynq device on the JTAG chain.""" name: str - part_number: str # e.g. "XC7Z100" - family: str # e.g. "zynq7" - speed: str # e.g. "-1" + part_number: str # e.g. "XC7Z100" + family: str # e.g. "zynq7" + speed: str # e.g. "-1" idcode: str = "" is_programmable: bool = False @@ -43,38 +46,58 @@ class ZynqDevice: class ZynqCheckResult: """Result of Zynq device detection on the JTAG chain.""" - devices: list[ZynqDevice] # All detected Zynq devices - jtag_chain: list[JtagDevice] # All devices on JTAG chain - is_present: bool # Whether any Zynq device was found - ps_detected: bool # Whether PS (ARM DAP) is detected - pl_detected: bool # Whether PL is detected - hw_server_url: str # hw_server connection URL used - error: str = "" # Error message if detection failed + devices: list[ZynqDevice] + jtag_chain: list[JtagDevice] + is_present: bool + ps_detected: bool + pl_detected: bool + hw_server_url: str + error: str = "" -# ── Patterns for parsing xsct "jtag targets" output ────────────────── -# Each line: " 1 xc7z100 (idcode 03736093 irlen 4)" -_JTAG_LINE_RE = re.compile( - r"^\s*(\d+)\s+(\S+)\s+\(idcode\s+([0-9A-Fa-f]+)\s+irlen\s+(\d+)\)", - re.IGNORECASE, -) +# ── Patterns ────────────────────────────────────────────────────────── +# Vivado TCL output: DEVICE:arm_dap_0 IDCODE:0100101110100...0111 +_DEVICE_LINE_RE = re.compile(r"^DEVICE:(\S+)\s+IDCODE:([01]+)") -# Zynq PL part: "xc7z100", "xc7z010", etc. -_ZYNQ_DEVICE_RE = re.compile(r"^xc\d+z\d+[a-z]?\d+$", re.IGNORECASE) +# Zynq PL: xc7z100, xc7z100_0, xc7z100_1, etc. +_ZYNQ_RE = re.compile(r"^xc\d+z\d+[a-z]?\d+", re.IGNORECASE) -# ARM DAP (PS side): "arm_dap" -_ARM_DAP_RE = re.compile(r"^arm_dap$", re.IGNORECASE) +# ARM DAP (PS): arm_dap, arm_dap_0, arm_dap_1 +_ARM_DAP_RE = re.compile(r"^arm_dap", 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. +# ── Helpers ─────────────────────────────────────────────────────────── - Connects to hw_server using Vivado batch mode, scans the JTAG chain, - and identifies Zynq devices by their JTAG device names. Also checks - for PS (ARM DAP) and PL (Programmable Logic) recognition. +def _port_open(host: str = "localhost", port: int = HW_SERVER_PORT) -> bool: + """Check if a TCP port is accepting connections.""" + try: + with socket.create_connection((host, port), timeout=1): + return True + except OSError: + return False + + +def _idcode_to_hex(binary_str: str) -> str: + """Convert Vivado's binary IDCODE to uppercase hex.""" + try: + return f"{int(binary_str, 2):08X}" + except ValueError: + return binary_str + + +# ── Main entry ──────────────────────────────────────────────────────── + +def check_zynq_jtag( + config: Config, callback: Callable | None = None +) -> ZynqCheckResult: + """Check for Zynq devices on the JTAG chain via Vivado + hw_server. + + Lifecycle: + 1. If hw_server is already running (port 3121 open) → reuse it + 2. If not running → start it, kill it when done + 3. Run Vivado batch TCL to enumerate JTAG devices Args: config: Application configuration. @@ -84,58 +107,76 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe ZynqCheckResult with detected devices and status. """ hw_server_url = f"localhost:{HW_SERVER_PORT}" + xilinx_root = getattr(config, "xilinx_path", "") if callback: callback("start", "Connecting to hw_server...") - xilinx_root = getattr(config, "xilinx_path", "") - - # ── 1. Find and start hw_server ────────────────────────── - hw_server_path = get_hw_server_path(xilinx_root=xilinx_root) - if not hw_server_path: + # ── 1. Find tools ────────────────────────────────────────── + vivado_path = get_vivado_path(xilinx_root=xilinx_root) + if not vivado_path: return ZynqCheckResult( devices=[], jtag_chain=[], is_present=False, ps_detected=False, pl_detected=False, hw_server_url=hw_server_url, - error="hw_server not found — install Vivado/Vitis hardware server", + error="Vivado not found", ) + hw_server_path = get_hw_server_path(xilinx_root=xilinx_root) + hw_server_was_running = _port_open() + + # ── 2. Start hw_server if needed ─────────────────────────── hw_proc: subprocess.Popen | None = None - try: - # Start hw_server in the background (needed for xsct connect) - hw_proc = subprocess.Popen( - build_tool_command(hw_server_path), - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - time.sleep(2) # let hw_server bind its port - - # ── 2. Find xsct (much lighter than Vivado for JTAG ops) ── - xsct_path = get_xsct_path(xilinx_root=xilinx_root) - if not xsct_path: + if not hw_server_was_running: + if not hw_server_path: return ZynqCheckResult( devices=[], jtag_chain=[], is_present=False, ps_detected=False, pl_detected=False, hw_server_url=hw_server_url, - error="xsct not found", + error="hw_server not found — install Vivado/Vitis", + ) + try: + hw_proc = subprocess.Popen( + build_tool_command(hw_server_path), + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + # Wait for port to become available + for _ in range(30): # 3s max + if _port_open(): + break + time.sleep(0.1) + else: + return ZynqCheckResult( + devices=[], jtag_chain=[], is_present=False, + ps_detected=False, pl_detected=False, + hw_server_url=hw_server_url, + error="hw_server started but port 3121 not open", + ) + except OSError as e: + return ZynqCheckResult( + devices=[], jtag_chain=[], is_present=False, + ps_detected=False, pl_detected=False, + hw_server_url=hw_server_url, + error=f"Failed to start hw_server: {e}", ) - # ── 3. Run JTAG query TCL via xsct ──────────────────── - tcl_content = _build_jtag_query_tcl() - + # ── 3. Run Vivado TCL ────────────────────────────────────── + try: + tcl = _build_jtag_query_tcl() if callback: callback("progress", "Scanning JTAG chain...") - cmd = build_tool_command( - xsct_path, "-eval", tcl_content, - ) python_timeout = config.step_timeouts.get("check_env", 120) result = subprocess.run( - cmd, - capture_output=True, - text=True, - timeout=python_timeout, + build_tool_command(vivado_path, "-mode", "batch", "-source", tcl), + capture_output=True, text=True, timeout=python_timeout, ) + # Clean up temp TCL file + try: + Path(tcl).unlink(missing_ok=True) + except Exception: + pass output = result.stdout + result.stderr jtag_chain = _parse_jtag_chain(output) @@ -176,7 +217,7 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe error=f"OS error: {e}", ) finally: - # ── 4. Kill hw_server ───────────────────────────────── + # ── 4. Kill hw_server only if WE started it ──────────── if hw_proc is not None and hw_proc.poll() is None: try: hw_proc.terminate() @@ -185,40 +226,44 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe hw_proc.kill() hw_proc.wait() except OSError: - pass # already dead + pass +# ── TCL builder ──────────────────────────────────────────────────────── + def _build_jtag_query_tcl() -> str: - """Build TCL script for xsct JTAG device query. - - Uses 'jtag targets' (not 'targets') to get raw JTAG chain devices - with IDCODE, irlen — the same info that hw_server exposes. + """Build Vivado TCL script file for JTAG device enumeration. Returns: - TCL script content as string (semicolon-delimited for -eval mode). + Path to the temporary TCL script file. """ - return ( - "connect; " - 'puts "===JTAG_START==="; ' - "jtag targets; " - 'puts "===JTAG_END==="; ' - "disconnect; " - "quit" - ) + content = """\ +open_hw_manager +connect_hw_server +open_hw_target +puts "===JTAG_START===" +foreach dev [get_hw_devices] { + set name [get_property NAME $dev] + set idcode [get_property IDCODE $dev] + puts "DEVICE:$name IDCODE:$idcode" +} +puts "===JTAG_END===" +quit +""" + import os + path = Path(tempfile.gettempdir()) / f"zynq_jtag_{os.getpid()}.tcl" + path.write_text(content) + return str(path) +# ── Parsers ──────────────────────────────────────────────────────────── + def _parse_jtag_chain(output: str) -> list[JtagDevice]: - """Parse xsct 'jtag targets' output into JtagDevice list. + """Parse Vivado TCL output into JtagDevice list. - Expected format (one line per JTAG device): - 1 xc7z100 (idcode 03736093 irlen 4) - 2 arm_dap (idcode 4BA00477 irlen 4) - - Args: - output: Combined stdout/stderr from xsct. - - Returns: - List of JtagDevice objects for all devices on the JTAG chain. + Expected format: + DEVICE:arm_dap_0 IDCODE:01001011101000000000010001110111 + DEVICE:xc7z100_1 IDCODE:00000011011100110110000010010011 """ devices: list[JtagDevice] = [] section = _extract_section(output, "===JTAG_START===", "===JTAG_END===") @@ -229,25 +274,17 @@ def _parse_jtag_chain(output: str) -> list[JtagDevice]: line = line.strip() if not line: continue - match = _JTAG_LINE_RE.match(line) + match = _DEVICE_LINE_RE.match(line) if match: - _idx = match.group(1) - name = match.group(2) - idcode = match.group(3) - devices.append(_classify_jtag_device(name, idcode)) + name = match.group(1) + idcode_hex = _idcode_to_hex(match.group(2)) + devices.append(_classify_jtag_device(name, idcode_hex)) return devices def _filter_zynq_devices(jtag_chain: list[JtagDevice]) -> list[ZynqDevice]: - """Filter Zynq devices from JTAG chain, propagating IDCODE. - - Args: - jtag_chain: List of all JTAG devices. - - Returns: - List of ZynqDevice objects. - """ + """Filter Zynq devices from JTAG chain, propagating IDCODE.""" zynq_devices: list[ZynqDevice] = [] for jtag_dev in jtag_chain: if jtag_dev.is_zynq: @@ -257,24 +294,8 @@ def _filter_zynq_devices(jtag_chain: list[JtagDevice]) -> list[ZynqDevice]: return zynq_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 +def _extract_section(text: str, start_marker: str, end_marker: str) -> str | None: + """Extract text between last occurrence of two markers.""" 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: @@ -283,16 +304,8 @@ def _extract_section( def _classify_jtag_device(device_name: str, idcode: str = "") -> JtagDevice: - """Classify a JTAG device by its name and IDCODE. - - Args: - device_name: Device name from 'jtag targets' (e.g. "xc7z100", "arm_dap"). - idcode: JTAG IDCODE hex string. - - Returns: - JtagDevice with classification information. - """ - is_zynq = bool(_ZYNQ_DEVICE_RE.match(device_name)) + """Classify a JTAG device by name (Vivado format: xc7z100_1, arm_dap_0).""" + is_zynq = bool(_ZYNQ_RE.match(device_name)) is_arm_dap = bool(_ARM_DAP_RE.match(device_name)) is_pl_device = is_zynq and not is_arm_dap @@ -306,32 +319,21 @@ def _classify_jtag_device(device_name: str, idcode: str = "") -> JtagDevice: def _parse_zynq_device(device_name: str, idcode: str = "") -> ZynqDevice | None: - """Parse a Zynq device from its JTAG chain name and IDCODE. - - Args: - device_name: Device name from JTAG chain (e.g. "xc7z100"). - idcode: JTAG IDCODE hex string. - - Returns: - ZynqDevice if it's a Zynq chip, None otherwise. - """ - match = _ZYNQ_DEVICE_RE.match(device_name) + """Parse Zynq device name (Vivado format: xc7z100_1).""" + match = _ZYNQ_RE.match(device_name) if not match: return None - part_number = device_name.upper() + # Strip index suffix: xc7z100_1 → XC7Z100 + base = device_name.rsplit("_", 1)[0] if "_" in device_name else device_name + part_number = base.upper() - # Extract family and speed from part number family = "zynq7" - speed = "-1" # Default speed grade - + speed = "-1" speed_match = re.search(r"(\d+)$", part_number) if speed_match: speed_num = int(speed_match.group(1)) - if speed_num >= 2: - speed = "-2" - elif speed_num >= 1: - speed = "-1" + speed = "-2" if speed_num >= 2 else "-1" return ZynqDevice( name=device_name, @@ -343,15 +345,10 @@ def _parse_zynq_device(device_name: str, idcode: str = "") -> ZynqDevice | None: ) +# ── Status dict ──────────────────────────────────────────────────────── + 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. - """ + """Convert ZynqCheckResult to a serializable dict.""" return { "is_present": result.is_present, "ps_detected": result.ps_detected, @@ -361,6 +358,7 @@ def get_zynq_status_dict(result: ZynqCheckResult) -> dict: "jtag_chain": [ { "name": dev.name, + "idcode": dev.idcode, "is_zynq": dev.is_zynq, "is_arm_dap": dev.is_arm_dap, "is_pl_device": dev.is_pl_device,