Compare commits

...
2 Commits
Author SHA1 Message Date
yuysh 91417f609e fix: start hw_server explicitly before Vivado JTAG scan
Previously the TCL script relied on Vivado's connect_hw_server to
auto-start hw_server. On Windows this either never worked (hw_server
never shows in Task Manager) or hung indefinitely during Vivado's
slow batch-mode initialisation.

Root cause: hw_server was never started.

Changes:
- vitis_checker.py: add 'hw_server' to TOOL_ALIASES + get_hw_server_path()
- zynq_checker.py: start hw_server via Popen before Vivado, kill in finally
  - 2s sleep for port binding after start
  - timeout from config.step_timeouts.check_env (default bumped to 120s)
  - clean terminate/kill sequence in finally
- config_manager.py: check_env default 30 → 120s
2026-06-11 15:57:53 +08:00
yuysh 72d0eceb8d fix: don't restart Boot Verify pulse after step 4 already completed it
Step 4's _run_step_4_load_main_program() already marks each sub-step
individually via set_step_complete/set_step_error. But the outer
_run_steps_worker() called set_phase('Boot Verify') on success, which
re-entered the 'running' phase and started a fresh pulse animation
(because _stop_pulse cleared _pulse_job).

Fix: on success, do nothing (sub-steps are already green). Only set
error on failure.
2026-06-11 15:49:10 +08:00
4 changed files with 79 additions and 42 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
"inter_step_delay": 2, "inter_step_delay": 2,
"boot_wait_delay": 10, "boot_wait_delay": 10,
"step_timeouts": { "step_timeouts": {
"check_env": 30, "check_env": 120,
"flash_erase": 120, "flash_erase": 120,
"flash_program": 600, "flash_program": 600,
"bitstream": 60, "bitstream": 60,
+3 -4
View File
@@ -1436,11 +1436,10 @@ class MainWindow(ctk.CTk):
else: else:
self._sub_step_frame.set_phase("error") self._sub_step_frame.set_phase("error")
# Step 4: mark sub-steps based on result # Step 4: sub-steps already marked individually inside _run_step_4;
# avoid set_phase which would restart a pulse on Boot Verify.
if i == 3 and hasattr(self, '_tftp_sub_step_frame'): if i == 3 and hasattr(self, '_tftp_sub_step_frame'):
if success: if not success:
self._tftp_sub_step_frame.set_phase("Boot Verify")
else:
self._tftp_sub_step_frame.set_step_error("Boot Verify") self._tftp_sub_step_frame.set_step_error("Boot Verify")
except Exception as e: except Exception as e:
results.append(False) results.append(False)
+19
View File
@@ -56,6 +56,7 @@ TOOL_ALIASES: dict[str, list[str]] = {
"xsct": ["xsct"], "xsct": ["xsct"],
"program_flash": ["program_flash"], "program_flash": ["program_flash"],
"bootgen": ["bootgen"], "bootgen": ["bootgen"],
"hw_server": ["hw_server"],
} }
# Product subdirectories under xilinx_root to scan (in preference order) # Product subdirectories under xilinx_root to scan (in preference order)
@@ -330,6 +331,24 @@ def get_vivado_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
return _find_executable("vivado", vitis_path, platform.system().lower(), xilinx_root) return _find_executable("vivado", vitis_path, platform.system().lower(), xilinx_root)
def get_hw_server_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
"""Find the hw_server executable (Xilinx hardware server).
hw_server must be running before Vivado/xsct can connect to the
JTAG chain. This finder searches the same Vitis/Vivado bin dirs.
Args:
vitis_path: Legacy Vitis installation directory.
xilinx_root: Xilinx root directory.
Returns:
Path to hw_server executable, or None.
"""
return _find_executable(
"hw_server", vitis_path, platform.system().lower(), xilinx_root
)
# ── Tool version detection ─────────────────────────────────────────── # ── Tool version detection ───────────────────────────────────────────
+56 -37
View File
@@ -12,12 +12,13 @@ import os
import re import re
import subprocess import subprocess
import tempfile import tempfile
import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path 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_vivado_path, build_tool_command from vitis_checker import get_vivado_path, get_hw_server_path, build_tool_command
@dataclass @dataclass
@@ -87,42 +88,60 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe
if callback: if callback:
callback("start", "Connecting to hw_server...") callback("start", "Connecting to hw_server...")
# Find vivado executable via Xilinx root scan xilinx_root = getattr(config, "xilinx_path", "")
vivado_path = get_vivado_path(
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 vivado_path: if not hw_server_path:
return ZynqCheckResult( return ZynqCheckResult(
devices=[], devices=[], jtag_chain=[], is_present=False,
jtag_chain=[], ps_detected=False, pl_detected=False,
is_present=False,
ps_detected=False,
pl_detected=False,
hw_server_url=hw_server_url, hw_server_url=hw_server_url,
error="Vivado not found", error="hw_server not found — install Vivado/Vitis hardware server",
) )
# Create temporary TCL script hw_proc: subprocess.Popen | None = None
tcl_content = _build_jtag_query_tcl()
tcl_path = Path(tempfile.gettempdir()) / f"zynq_jtag_check_{os.getpid()}.tcl"
try: try:
# Start hw_server in the background (it daemonises own I/O)
hw_proc = subprocess.Popen(
build_tool_command(hw_server_path),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# Give hw_server a moment to bind its port
time.sleep(2)
# ── 2. Find vivado ────────────────────────────────────
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="Vivado not found",
)
# ── 3. Create and run JTAG query TCL ──────────────────
tcl_content = _build_jtag_query_tcl()
tcl_path = Path(tempfile.gettempdir()) / f"zynq_jtag_check_{os.getpid()}.tcl"
tcl_path.write_text(tcl_content) tcl_path.write_text(tcl_content)
if callback: if callback:
callback("progress", "Scanning JTAG chain...") callback("progress", "Scanning JTAG chain...")
# Run Vivado in batch mode (with environment setup) cmd = build_tool_command(
cmd = build_tool_command(vivado_path, "-mode", "batch", "-source", str(tcl_path)) vivado_path, "-mode", "batch", "-source", str(tcl_path),
)
python_timeout = config.step_timeouts.get("check_env", 120)
result = subprocess.run( result = subprocess.run(
cmd, cmd,
capture_output=True, capture_output=True,
text=True, text=True,
timeout=30, timeout=python_timeout,
) )
tcl_path.unlink(missing_ok=True)
output = result.stdout + result.stderr output = result.stdout + result.stderr
# Parse JTAG device information from output
jtag_chain = _parse_jtag_chain(output) jtag_chain = _parse_jtag_chain(output)
devices = _filter_zynq_devices(jtag_chain) devices = _filter_zynq_devices(jtag_chain)
@@ -135,42 +154,42 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe
else: else:
callback("error", "No Zynq device found on JTAG chain") 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) ps_detected = any(d.is_arm_dap for d in jtag_chain)
pl_detected = any(d.is_pl_device for d in jtag_chain) pl_detected = any(d.is_pl_device for d in jtag_chain)
return ZynqCheckResult( return ZynqCheckResult(
devices=devices, devices=devices, jtag_chain=jtag_chain,
jtag_chain=jtag_chain,
is_present=len(devices) > 0, is_present=len(devices) > 0,
ps_detected=ps_detected, ps_detected=ps_detected, pl_detected=pl_detected,
pl_detected=pl_detected,
hw_server_url=hw_server_url, hw_server_url=hw_server_url,
error="" if devices else "No Zynq device found on JTAG chain", error="" if devices else "No Zynq device found on JTAG chain",
) )
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
return ZynqCheckResult( return ZynqCheckResult(
devices=[], devices=[], jtag_chain=[], is_present=False,
jtag_chain=[], ps_detected=False, pl_detected=False,
is_present=False,
ps_detected=False,
pl_detected=False,
hw_server_url=hw_server_url, hw_server_url=hw_server_url,
error="JTAG scan timed out after 30s", error=f"JTAG scan timed out after {config.step_timeouts.get('check_env', 120)}s",
) )
except OSError as e: except OSError as e:
return ZynqCheckResult( return ZynqCheckResult(
devices=[], devices=[], jtag_chain=[], is_present=False,
jtag_chain=[], ps_detected=False, pl_detected=False,
is_present=False,
ps_detected=False,
pl_detected=False,
hw_server_url=hw_server_url, hw_server_url=hw_server_url,
error=f"OS error: {e}", error=f"OS error: {e}",
) )
finally: finally:
tcl_path.unlink(missing_ok=True) # ── 4. Kill hw_server ─────────────────────────────────
if hw_proc is not None and hw_proc.poll() is None:
try:
hw_proc.terminate()
hw_proc.wait(timeout=3)
except subprocess.TimeoutExpired:
hw_proc.kill()
hw_proc.wait()
except OSError:
pass # already dead
def _build_jtag_query_tcl() -> str: def _build_jtag_query_tcl() -> str: