Files
Zynq_Flasher/src/zynq_checker.py
T
Jeremy Shen 62609b6e80 feat(jtag): enhance Zynq JTAG detection with PS/PL status
- Remove IP check from Step 1 environment check
- Add PS (ARM DAP) and PL detection to zynq_checker
- Log detailed JTAG chain information in Step 1
- Show PS/PL detection status in GUI logs
- Display JTAG chain devices with type classification
2026-06-10 09:43:02 +08:00

388 lines
11 KiB
Python

"""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.
Checks for both PS (Processing System) and PL (Programmable Logic)
recognition on the JTAG chain.
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable
from config_manager import Config
@dataclass
class JtagDevice:
"""Information about a device on the JTAG chain."""
name: str # Device name from JTAG chain (e.g. "xc7z100_1")
is_zynq: bool # Whether this is a Zynq device
is_arm_dap: bool # Whether this is an ARM DAP (PS side)
is_pl_device: bool # Whether this is a PL device
@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
is_programmable: bool = False # Whether the device is programmable
@dataclass
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
# 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
)
# ARM DAP pattern (PS side)
ARM_DAP_PATTERN = re.compile(r"^arm_dap_\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. Also checks
for PS (ARM DAP) and PL (Programmable Logic) recognition.
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=[],
jtag_chain=[],
is_present=False,
ps_detected=False,
pl_detected=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
jtag_chain = _parse_jtag_chain(output)
devices = _filter_zynq_devices(jtag_chain)
if callback:
if devices:
ps_note = "PS" if any(d.is_arm_dap for d in jtag_chain) else ""
pl_note = "PL" if any(d.is_pl_device for d in jtag_chain) else ""
notes = ", ".join(filter(None, [ps_note, pl_note]))
callback("complete", f"Found {len(devices)} Zynq device(s) [{notes}]")
else:
callback("error", "No Zynq device found on JTAG chain")
# Check PS and PL detection
ps_detected = any(d.is_arm_dap for d in jtag_chain)
pl_detected = any(d.is_pl_device for d in jtag_chain)
return ZynqCheckResult(
devices=devices,
jtag_chain=jtag_chain,
is_present=len(devices) > 0,
ps_detected=ps_detected,
pl_detected=pl_detected,
hw_server_url=hw_server_url,
error="" if devices else "No Zynq device found on JTAG chain",
)
except subprocess.TimeoutExpired:
return ZynqCheckResult(
devices=[],
jtag_chain=[],
is_present=False,
ps_detected=False,
pl_detected=False,
hw_server_url=hw_server_url,
error="JTAG scan timed out after 30s",
)
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"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_chain(output: str) -> list[JtagDevice]:
"""Parse JTAG chain information from Vivado output.
Args:
output: Combined stdout/stderr from Vivado batch execution.
Returns:
List of JtagDevice objects for all devices on the JTAG chain.
"""
devices: list[JtagDevice] = []
# 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 = _classify_jtag_device(device_name)
devices.append(device)
return devices
def _filter_zynq_devices(jtag_chain: list[JtagDevice]) -> list[ZynqDevice]:
"""Filter Zynq devices from JTAG chain.
Args:
jtag_chain: List of all JTAG devices.
Returns:
List of ZynqDevice objects.
"""
zynq_devices: list[ZynqDevice] = []
for jtag_dev in jtag_chain:
if jtag_dev.is_zynq:
zynq_dev = _parse_zynq_device(jtag_dev.name)
if zynq_dev:
zynq_devices.append(zynq_dev)
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
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 _classify_jtag_device(device_name: str) -> JtagDevice:
"""Classify a JTAG device by its name.
Args:
device_name: Device name from JTAG chain.
Returns:
JtagDevice with classification information.
"""
is_zynq = bool(ZYNQ_PART_PATTERN.match(device_name))
is_arm_dap = bool(ARM_DAP_PATTERN.match(device_name))
is_pl_device = is_zynq and not is_arm_dap
return JtagDevice(
name=device_name,
is_zynq=is_zynq,
is_arm_dap=is_arm_dap,
is_pl_device=is_pl_device,
)
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,
"ps_detected": result.ps_detected,
"pl_detected": result.pl_detected,
"hw_server_url": result.hw_server_url,
"error": result.error,
"jtag_chain": [
{
"name": dev.name,
"is_zynq": dev.is_zynq,
"is_arm_dap": dev.is_arm_dap,
"is_pl_device": dev.is_pl_device,
}
for dev in result.jtag_chain
],
"devices": [
{
"name": dev.name,
"part_number": dev.part_number,
"family": dev.family,
"speed": dev.speed,
"idcode": dev.idcode,
"is_programmable": dev.is_programmable,
}
for dev in result.devices
],
}