feat(jtag): add Zynq JTAG presence detection via hw_server

- Add zynq_checker.py module to detect Zynq devices on JTAG chain
- Connects to hw_server via Vivado batch mode
- Parses JTAG device names to extract part number (e.g. XC7Z100)
- Integrates into Step 1 environment check in main_window.py
- Shows JTAG status in header (JTAG: XC7Z100 ✓)
This commit is contained in:
Jeremy Shen
2026-06-09 18:33:50 +08:00
parent 90c3dca8b8
commit 2bde6d869e
2 changed files with 327 additions and 0 deletions
+297
View File
@@ -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
],
}