fix: use Vivado TCL for JTAG check — xsct 'targets' has broken TCL list parsing

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.
This commit is contained in:
2026-06-11 17:02:23 +08:00
parent dd35ab0846
commit 474e3f122b
+150 -152
View File
@@ -1,27 +1,30 @@
"""Zynq JTAG presence checker for Zynq Flasher GUI. """Zynq JTAG presence checker for Zynq Flasher GUI.
Connects to hw_server via xsct and scans the JTAG chain to detect Uses Vivado batch-mode TCL to scan the JTAG chain via hw_server,
Zynq devices, returning chip model, IDCODE, PS/PL presence. returning device names, IDCODEs, and PS/PL presence.
""" """
from __future__ import annotations from __future__ import annotations
import re import re
import socket
import subprocess import subprocess
import tempfile
import time import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable from typing import Callable
from config_manager import Config 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 @dataclass
class JtagDevice: class JtagDevice:
"""Information about a device on the JTAG chain.""" """Information about a device on the JTAG chain."""
name: str # Device name (e.g. "xc7z100", "arm_dap") name: str
idcode: str = "" # JTAG IDCODE hex string idcode: str = "" # IDCODE in hex (e.g. "03736093")
is_zynq: bool = False is_zynq: bool = False
is_arm_dap: bool = False is_arm_dap: bool = False
is_pl_device: bool = False is_pl_device: bool = False
@@ -32,9 +35,9 @@ class ZynqDevice:
"""Information about a detected Zynq device on the JTAG chain.""" """Information about a detected Zynq device on the JTAG chain."""
name: str name: str
part_number: str # e.g. "XC7Z100" part_number: str # e.g. "XC7Z100"
family: str # e.g. "zynq7" family: str # e.g. "zynq7"
speed: str # e.g. "-1" speed: str # e.g. "-1"
idcode: str = "" idcode: str = ""
is_programmable: bool = False is_programmable: bool = False
@@ -43,38 +46,58 @@ class ZynqDevice:
class ZynqCheckResult: class ZynqCheckResult:
"""Result of Zynq device detection on the JTAG chain.""" """Result of Zynq device detection on the JTAG chain."""
devices: list[ZynqDevice] # All detected Zynq devices devices: list[ZynqDevice]
jtag_chain: list[JtagDevice] # All devices on JTAG chain jtag_chain: list[JtagDevice]
is_present: bool # Whether any Zynq device was found is_present: bool
ps_detected: bool # Whether PS (ARM DAP) is detected ps_detected: bool
pl_detected: bool # Whether PL is detected pl_detected: bool
hw_server_url: str # hw_server connection URL used hw_server_url: str
error: str = "" # Error message if detection failed error: str = ""
# ── Patterns for parsing xsct "jtag targets" output ────────────────── # ── Patterns ──────────────────────────────────────────────────────────
# Each line: " 1 xc7z100 (idcode 03736093 irlen 4)" # Vivado TCL output: DEVICE:arm_dap_0 IDCODE:0100101110100...0111
_JTAG_LINE_RE = re.compile( _DEVICE_LINE_RE = re.compile(r"^DEVICE:(\S+)\s+IDCODE:([01]+)")
r"^\s*(\d+)\s+(\S+)\s+\(idcode\s+([0-9A-Fa-f]+)\s+irlen\s+(\d+)\)",
re.IGNORECASE,
)
# Zynq PL part: "xc7z100", "xc7z010", etc. # Zynq PL: xc7z100, xc7z100_0, xc7z100_1, etc.
_ZYNQ_DEVICE_RE = re.compile(r"^xc\d+z\d+[a-z]?\d+$", re.IGNORECASE) _ZYNQ_RE = re.compile(r"^xc\d+z\d+[a-z]?\d+", re.IGNORECASE)
# ARM DAP (PS side): "arm_dap" # ARM DAP (PS): arm_dap, arm_dap_0, arm_dap_1
_ARM_DAP_RE = re.compile(r"^arm_dap$", re.IGNORECASE) _ARM_DAP_RE = re.compile(r"^arm_dap", re.IGNORECASE)
# hw_server default port
HW_SERVER_PORT = 3121 HW_SERVER_PORT = 3121
def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqCheckResult: # ── Helpers ───────────────────────────────────────────────────────────
"""Check for Zynq devices on the JTAG chain via hw_server.
Connects to hw_server using Vivado batch mode, scans the JTAG chain, def _port_open(host: str = "localhost", port: int = HW_SERVER_PORT) -> bool:
and identifies Zynq devices by their JTAG device names. Also checks """Check if a TCP port is accepting connections."""
for PS (ARM DAP) and PL (Programmable Logic) recognition. 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: Args:
config: Application configuration. config: Application configuration.
@@ -84,58 +107,76 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe
ZynqCheckResult with detected devices and status. ZynqCheckResult with detected devices and status.
""" """
hw_server_url = f"localhost:{HW_SERVER_PORT}" hw_server_url = f"localhost:{HW_SERVER_PORT}"
xilinx_root = getattr(config, "xilinx_path", "")
if callback: if callback:
callback("start", "Connecting to hw_server...") callback("start", "Connecting to hw_server...")
xilinx_root = getattr(config, "xilinx_path", "") # ── 1. Find tools ──────────────────────────────────────────
vivado_path = get_vivado_path(xilinx_root=xilinx_root)
# ── 1. Find and start hw_server ────────────────────────── if not vivado_path:
hw_server_path = get_hw_server_path(xilinx_root=xilinx_root)
if not hw_server_path:
return ZynqCheckResult( return ZynqCheckResult(
devices=[], jtag_chain=[], is_present=False, devices=[], jtag_chain=[], is_present=False,
ps_detected=False, pl_detected=False, ps_detected=False, pl_detected=False,
hw_server_url=hw_server_url, 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 hw_proc: subprocess.Popen | None = None
try: if not hw_server_was_running:
# Start hw_server in the background (needed for xsct connect) if not hw_server_path:
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:
return ZynqCheckResult( return ZynqCheckResult(
devices=[], jtag_chain=[], is_present=False, devices=[], jtag_chain=[], is_present=False,
ps_detected=False, pl_detected=False, ps_detected=False, pl_detected=False,
hw_server_url=hw_server_url, 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 ──────────────────── # ── 3. Run Vivado TCL ──────────────────────────────────────
tcl_content = _build_jtag_query_tcl() try:
tcl = _build_jtag_query_tcl()
if callback: if callback:
callback("progress", "Scanning JTAG chain...") callback("progress", "Scanning JTAG chain...")
cmd = build_tool_command(
xsct_path, "-eval", tcl_content,
)
python_timeout = config.step_timeouts.get("check_env", 120) python_timeout = config.step_timeouts.get("check_env", 120)
result = subprocess.run( result = subprocess.run(
cmd, build_tool_command(vivado_path, "-mode", "batch", "-source", tcl),
capture_output=True, capture_output=True, text=True, timeout=python_timeout,
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 output = result.stdout + result.stderr
jtag_chain = _parse_jtag_chain(output) 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}", error=f"OS error: {e}",
) )
finally: 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: if hw_proc is not None and hw_proc.poll() is None:
try: try:
hw_proc.terminate() hw_proc.terminate()
@@ -185,40 +226,44 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe
hw_proc.kill() hw_proc.kill()
hw_proc.wait() hw_proc.wait()
except OSError: except OSError:
pass # already dead pass
# ── TCL builder ────────────────────────────────────────────────────────
def _build_jtag_query_tcl() -> str: def _build_jtag_query_tcl() -> str:
"""Build TCL script for xsct JTAG device query. """Build Vivado TCL script file for JTAG device enumeration.
Uses 'jtag targets' (not 'targets') to get raw JTAG chain devices
with IDCODE, irlen — the same info that hw_server exposes.
Returns: Returns:
TCL script content as string (semicolon-delimited for -eval mode). Path to the temporary TCL script file.
""" """
return ( content = """\
"connect; " open_hw_manager
'puts "===JTAG_START==="; ' connect_hw_server
"jtag targets; " open_hw_target
'puts "===JTAG_END==="; ' puts "===JTAG_START==="
"disconnect; " foreach dev [get_hw_devices] {
"quit" 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]: 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): Expected format:
1 xc7z100 (idcode 03736093 irlen 4) DEVICE:arm_dap_0 IDCODE:01001011101000000000010001110111
2 arm_dap (idcode 4BA00477 irlen 4) DEVICE:xc7z100_1 IDCODE:00000011011100110110000010010011
Args:
output: Combined stdout/stderr from xsct.
Returns:
List of JtagDevice objects for all devices on the JTAG chain.
""" """
devices: list[JtagDevice] = [] devices: list[JtagDevice] = []
section = _extract_section(output, "===JTAG_START===", "===JTAG_END===") section = _extract_section(output, "===JTAG_START===", "===JTAG_END===")
@@ -229,25 +274,17 @@ def _parse_jtag_chain(output: str) -> list[JtagDevice]:
line = line.strip() line = line.strip()
if not line: if not line:
continue continue
match = _JTAG_LINE_RE.match(line) match = _DEVICE_LINE_RE.match(line)
if match: if match:
_idx = match.group(1) name = match.group(1)
name = match.group(2) idcode_hex = _idcode_to_hex(match.group(2))
idcode = match.group(3) devices.append(_classify_jtag_device(name, idcode_hex))
devices.append(_classify_jtag_device(name, idcode))
return devices return devices
def _filter_zynq_devices(jtag_chain: list[JtagDevice]) -> list[ZynqDevice]: def _filter_zynq_devices(jtag_chain: list[JtagDevice]) -> list[ZynqDevice]:
"""Filter Zynq devices from JTAG chain, propagating IDCODE. """Filter Zynq devices from JTAG chain, propagating IDCODE."""
Args:
jtag_chain: List of all JTAG devices.
Returns:
List of ZynqDevice objects.
"""
zynq_devices: list[ZynqDevice] = [] zynq_devices: list[ZynqDevice] = []
for jtag_dev in jtag_chain: for jtag_dev in jtag_chain:
if jtag_dev.is_zynq: if jtag_dev.is_zynq:
@@ -257,24 +294,8 @@ def _filter_zynq_devices(jtag_chain: list[JtagDevice]) -> list[ZynqDevice]:
return zynq_devices return zynq_devices
def _extract_section( def _extract_section(text: str, start_marker: str, end_marker: str) -> str | None:
text: str, start_marker: str, end_marker: str """Extract text between last occurrence of two markers."""
) -> 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) start_idx = text.rfind(start_marker)
end_idx = text.rfind(end_marker) end_idx = text.rfind(end_marker)
if start_idx == -1 or end_idx == -1 or start_idx >= end_idx: 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: def _classify_jtag_device(device_name: str, idcode: str = "") -> JtagDevice:
"""Classify a JTAG device by its name and IDCODE. """Classify a JTAG device by name (Vivado format: xc7z100_1, arm_dap_0)."""
is_zynq = bool(_ZYNQ_RE.match(device_name))
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))
is_arm_dap = bool(_ARM_DAP_RE.match(device_name)) is_arm_dap = bool(_ARM_DAP_RE.match(device_name))
is_pl_device = is_zynq and not is_arm_dap 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: def _parse_zynq_device(device_name: str, idcode: str = "") -> ZynqDevice | None:
"""Parse a Zynq device from its JTAG chain name and IDCODE. """Parse Zynq device name (Vivado format: xc7z100_1)."""
match = _ZYNQ_RE.match(device_name)
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)
if not match: if not match:
return None 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" family = "zynq7"
speed = "-1" # Default speed grade speed = "-1"
speed_match = re.search(r"(\d+)$", part_number) speed_match = re.search(r"(\d+)$", part_number)
if speed_match: if speed_match:
speed_num = int(speed_match.group(1)) speed_num = int(speed_match.group(1))
if speed_num >= 2: speed = "-2" if speed_num >= 2 else "-1"
speed = "-2"
elif speed_num >= 1:
speed = "-1"
return ZynqDevice( return ZynqDevice(
name=device_name, 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: def get_zynq_status_dict(result: ZynqCheckResult) -> dict:
"""Convert a ZynqCheckResult to a serializable dictionary. """Convert ZynqCheckResult to a serializable dict."""
Args:
result: ZynqCheckResult from check_zynq_jtag().
Returns:
Dictionary suitable for JSON serialization or GUI display.
"""
return { return {
"is_present": result.is_present, "is_present": result.is_present,
"ps_detected": result.ps_detected, "ps_detected": result.ps_detected,
@@ -361,6 +358,7 @@ def get_zynq_status_dict(result: ZynqCheckResult) -> dict:
"jtag_chain": [ "jtag_chain": [
{ {
"name": dev.name, "name": dev.name,
"idcode": dev.idcode,
"is_zynq": dev.is_zynq, "is_zynq": dev.is_zynq,
"is_arm_dap": dev.is_arm_dap, "is_arm_dap": dev.is_arm_dap,
"is_pl_device": dev.is_pl_device, "is_pl_device": dev.is_pl_device,