✨ 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:
@@ -16,6 +16,7 @@ import customtkinter as ctk
|
|||||||
|
|
||||||
from config_manager import Config
|
from config_manager import Config
|
||||||
from vitis_checker import check_vitis, get_tool_status_dict
|
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 flash_programmer import full_flash_program, FlashResult
|
||||||
from bitstream_programmer import full_bitstream_program, BitstreamResult
|
from bitstream_programmer import full_bitstream_program, BitstreamResult
|
||||||
from ip_verifier import ping_ip
|
from ip_verifier import ping_ip
|
||||||
@@ -75,6 +76,8 @@ class MainWindow(ctk.CTk):
|
|||||||
self._worker_thread: threading.Thread | None = None
|
self._worker_thread: threading.Thread | None = None
|
||||||
self._uart_available: bool = False
|
self._uart_available: bool = False
|
||||||
self._uart_message: str = ""
|
self._uart_message: str = ""
|
||||||
|
self._zynq_jtag_present: bool = False
|
||||||
|
self._zynq_jtag_info = None
|
||||||
|
|
||||||
self._load_config()
|
self._load_config()
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
@@ -185,6 +188,13 @@ class MainWindow(ctk.CTk):
|
|||||||
)
|
)
|
||||||
self._ip_status.grid(row=0, column=2, sticky="e", padx=PADDING)
|
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:
|
def _build_workflow_panel(self, parent: ctk.CTkFrame) -> None:
|
||||||
"""Build the step-by-step workflow panel."""
|
"""Build the step-by-step workflow panel."""
|
||||||
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
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)
|
self._ip_status.configure(text=f"IP: {ip} ✗", text_color=DANGER_COLOR)
|
||||||
return False
|
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
|
# Check UART availability
|
||||||
self._log_message(" Checking UART serial port...")
|
self._log_message(" Checking UART serial port...")
|
||||||
port = self._config.serial_port
|
port = self._config.serial_port
|
||||||
@@ -568,6 +594,10 @@ class MainWindow(ctk.CTk):
|
|||||||
"""Callback for flash programming progress."""
|
"""Callback for flash programming progress."""
|
||||||
self._log_message(f" [{status}] {message}")
|
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:
|
def _run_step_3_load_bootloader(self) -> bool:
|
||||||
"""Step 3: Load bootloader (BIT + ELF) and verify."""
|
"""Step 3: Load bootloader (BIT + ELF) and verify."""
|
||||||
if not self._config:
|
if not self._config:
|
||||||
|
|||||||
@@ -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
|
||||||
|
],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user