♻️ refactor(toolchain): replace impact/bootgen with xsct/program_flash + add fsbl
BREAKING: Replaces obsolete impact (ISE-era) and misused bootgen with the correct Xilinx 2018.3–2023.2 toolchain. Toolchain changes: - vitis_checker: impact → xsct + program_flash + bootgen detection - flash_programmer: bootgen/impact → program_flash (erase + program + verify) - bitstream_programmer: impact → xsct fpga/dow (primary), vivado (fallback) - Removed duplicated _get_impact_path() — now shared from vitis_checker Config additions: - fsbl_elf_path: FSBL ELF required by program_flash for QSPI init - flash_type: QSPI mode (default: qspi-x4-single) GUI additions: - FSBL ELF file selector in Configuration panel - Step 2 validates FSBL ELF is selected - Step 1 now shows xsct/program_flash/bootgen in tool check
This commit is contained in:
@@ -19,7 +19,12 @@ bootloader_bit_path: ""
|
|||||||
bootloader_elf_path: ""
|
bootloader_elf_path: ""
|
||||||
|
|
||||||
# Flash programming (Step 2: Program & Verify Flash)
|
# Flash programming (Step 2: Program & Verify Flash)
|
||||||
|
# bootloader_bin_path is the bootloader image to program into QSPI flash
|
||||||
|
# fsbl_elf_path is the FSBL (First Stage Boot Loader) required by program_flash
|
||||||
|
# flash_type sets the QSPI interface mode (qspi-x4-single, qspi-x8-dual_parallel, etc.)
|
||||||
bootloader_bin_path: ""
|
bootloader_bin_path: ""
|
||||||
|
fsbl_elf_path: ""
|
||||||
|
flash_type: "qspi-x4-single"
|
||||||
|
|
||||||
# Firmware upload (Step 4: Load Main Program via TFTP)
|
# Firmware upload (Step 4: Load Main Program via TFTP)
|
||||||
firmware_bin_path: ""
|
firmware_bin_path: ""
|
||||||
|
|||||||
+231
-192
@@ -1,13 +1,16 @@
|
|||||||
"""Bitstream programming module for Zynq Flasher GUI.
|
"""Bitstream programming module for Zynq Flasher GUI.
|
||||||
|
|
||||||
Handles programming the Zynq device with a .bit bitstream file
|
Downloads a .bit bitstream to the Zynq PL fabric and runs a .elf
|
||||||
and running an .elf executable afterward.
|
executable on the PS ARM core.
|
||||||
|
|
||||||
|
Uses xsct (Xilinx Software Command Line Tool) as the primary tool
|
||||||
|
for JTAG operations, with Vivado batch mode as a fallback.
|
||||||
|
Compatible with Xilinx 2018.3 — 2023.2.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import platform
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -15,11 +18,14 @@ 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
|
||||||
|
|
||||||
|
# ── Types ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BitstreamResult:
|
class BitstreamResult:
|
||||||
"""Result of a bitstream programming operation."""
|
"""Result of a bitstream programming or ELF execution operation."""
|
||||||
|
|
||||||
step: str
|
step: str
|
||||||
success: bool
|
success: bool
|
||||||
@@ -29,93 +35,95 @@ class BitstreamResult:
|
|||||||
|
|
||||||
ProgressCallback = Callable[[str, str], None] | None
|
ProgressCallback = Callable[[str, str], None] | None
|
||||||
|
|
||||||
|
# ── Helpers ─────────────────────────────────────────────────────────
|
||||||
def _get_impact_path(vitis_path: str) -> str | None:
|
|
||||||
"""Find the impact executable path.
|
|
||||||
|
|
||||||
Tries 'impact' first, then falls back to 'xsct' (Xilinx 2023.2+).
|
|
||||||
Uses shutil.which() for cross-platform compatibility,
|
|
||||||
then falls back to common Vitis paths.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
vitis_path: Vitis installation path.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to impact or xsct executable, or None if not found.
|
|
||||||
"""
|
|
||||||
# Try shutil.which for impact, then xsct as fallback
|
|
||||||
for tool in ("impact", "xsct"):
|
|
||||||
path = shutil.which(tool)
|
|
||||||
if path:
|
|
||||||
return path
|
|
||||||
if vitis_path:
|
|
||||||
bin_dir = Path(vitis_path) / "bin"
|
|
||||||
if bin_dir.exists():
|
|
||||||
for tool in ("impact", "xsct"):
|
|
||||||
for ext in ("", ".exe"):
|
|
||||||
candidate = bin_dir / f"{tool}{ext}"
|
|
||||||
if candidate.exists():
|
|
||||||
return str(candidate)
|
|
||||||
common_paths = [
|
|
||||||
"/opt/xilinx/bin/impact",
|
|
||||||
os.path.expanduser("~/Xilinx/Vitis/bin/impact"),
|
|
||||||
]
|
|
||||||
for p in common_paths:
|
|
||||||
if os.path.isfile(p):
|
|
||||||
return p
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_vivado_path(vitis_path: str) -> str | None:
|
def _get_vivado_path(vitis_path: str) -> str | None:
|
||||||
"""Find the Vivado executable path.
|
"""Find the Vivado executable.
|
||||||
|
|
||||||
Uses shutil.which() for cross-platform compatibility,
|
Uses shutil.which() for PATH resolution, then common install paths.
|
||||||
then falls back to common Vitis paths.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
vitis_path: Vitis installation path.
|
vitis_path: Optional Vitis installation directory.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to Vivado executable, or None if not found.
|
Path to vivado executable, or None.
|
||||||
"""
|
"""
|
||||||
path = shutil.which("vivado")
|
found = shutil.which("vivado")
|
||||||
if path:
|
if found:
|
||||||
return path
|
return found
|
||||||
if vitis_path:
|
if vitis_path:
|
||||||
candidate = Path(vitis_path) / "bin" / "vivado"
|
candidate = Path(vitis_path) / "bin" / "vivado"
|
||||||
if candidate.exists():
|
if candidate.is_file():
|
||||||
return str(candidate)
|
return str(candidate)
|
||||||
candidate = Path(vitis_path) / "bin" / "vivado.exe"
|
common = [
|
||||||
if candidate.exists():
|
"/data/xilinx/Vivado/2023.2/bin/vivado",
|
||||||
return str(candidate)
|
|
||||||
common_paths = [
|
|
||||||
"/opt/xilinx/bin/vivado",
|
"/opt/xilinx/bin/vivado",
|
||||||
os.path.expanduser("~/Xilinx/Vivado/bin/vivado"),
|
os.path.expanduser("~/Xilinx/Vivado/2023.2/bin/vivado"),
|
||||||
]
|
]
|
||||||
for p in common_paths:
|
for p in common:
|
||||||
if os.path.isfile(p):
|
if os.path.isfile(p):
|
||||||
return p
|
return p
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _run_command(
|
||||||
|
cmd: list[str],
|
||||||
|
timeout: int = 120,
|
||||||
|
callback: ProgressCallback = None,
|
||||||
|
) -> subprocess.CompletedProcess[str]:
|
||||||
|
"""Run a subprocess command with progress reporting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd: Command and arguments.
|
||||||
|
timeout: Maximum seconds.
|
||||||
|
callback: Optional callback(status, message).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CompletedProcess result.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
subprocess.TimeoutExpired: If command exceeds timeout.
|
||||||
|
"""
|
||||||
|
if callback:
|
||||||
|
callback("running", f"Executing: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
if callback:
|
||||||
|
if result.returncode == 0:
|
||||||
|
callback("complete", f"{cmd[0]} succeeded")
|
||||||
|
else:
|
||||||
|
callback("error", f"{cmd[0]} failed (exit {result.returncode})")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ── Bitstream Download ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def program_bitstream(
|
def program_bitstream(
|
||||||
config: Config,
|
config: Config,
|
||||||
bit_path: str | Path,
|
bit_path: str | Path,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> BitstreamResult:
|
) -> BitstreamResult:
|
||||||
"""Program the Zynq device with a .bit bitstream file.
|
"""Download a .bit bitstream to the Zynq PL (FPGA fabric).
|
||||||
|
|
||||||
Uses Vivado/impact to download the bitstream to the FPGA fabric.
|
Attempts xsct first (fast, lightweight), then falls back to Vivado
|
||||||
|
batch mode.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
bit_path: Path to the .bit bitstream file.
|
bit_path: Path to the .bit file.
|
||||||
callback: Optional progress callback.
|
callback: Optional progress callback.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
BitstreamResult with status.
|
BitstreamResult with status.
|
||||||
"""
|
"""
|
||||||
bit_path = Path(bit_path).resolve()
|
bit_path = Path(bit_path)
|
||||||
if not bit_path.exists():
|
if not bit_path.exists():
|
||||||
return BitstreamResult(
|
return BitstreamResult(
|
||||||
step="bitstream",
|
step="bitstream",
|
||||||
@@ -126,21 +134,70 @@ def program_bitstream(
|
|||||||
if callback:
|
if callback:
|
||||||
callback("start", f"Loading bitstream: {bit_path.name}...")
|
callback("start", f"Loading bitstream: {bit_path.name}...")
|
||||||
|
|
||||||
vivado = _get_vivado_path(config.vitis_path)
|
# Try xsct first (preferred)
|
||||||
impact = _get_impact_path(config.vitis_path)
|
xsct = get_xsct_path(config.vitis_path)
|
||||||
|
if xsct:
|
||||||
|
return _program_with_xsct(xsct, bit_path, callback)
|
||||||
|
|
||||||
# Try Vivado first (preferred for Zynq)
|
# Fallback to Vivado
|
||||||
|
vivado = _get_vivado_path(config.vitis_path)
|
||||||
if vivado:
|
if vivado:
|
||||||
return _program_with_vivado(vivado, bit_path, callback)
|
return _program_with_vivado(vivado, bit_path, callback)
|
||||||
|
|
||||||
# Fallback to impact
|
|
||||||
if impact:
|
|
||||||
return _program_with_impact(impact, bit_path, callback)
|
|
||||||
|
|
||||||
return BitstreamResult(
|
return BitstreamResult(
|
||||||
step="bitstream",
|
step="bitstream",
|
||||||
success=False,
|
success=False,
|
||||||
message="Neither vivado nor impact found",
|
message="Neither xsct nor vivado found in PATH",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _program_with_xsct(
|
||||||
|
xsct_path: str,
|
||||||
|
bit_path: Path,
|
||||||
|
callback: ProgressCallback = None,
|
||||||
|
) -> BitstreamResult:
|
||||||
|
"""Program bitstream using xsct's fpga command.
|
||||||
|
|
||||||
|
xsct connects to hw_server and uses the `fpga` command to download
|
||||||
|
the bitstream to the PL fabric.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xsct_path: Path to xsct executable.
|
||||||
|
bit_path: Path to .bit file.
|
||||||
|
callback: Optional progress callback.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BitstreamResult with status.
|
||||||
|
"""
|
||||||
|
if callback:
|
||||||
|
callback("progress", "Using xsct to download bitstream...")
|
||||||
|
|
||||||
|
# xsct TCL script: connect to hw_server, target FPGA, download bitstream
|
||||||
|
tcl_script = f"""
|
||||||
|
connect
|
||||||
|
targets
|
||||||
|
fpga -file "{bit_path}"
|
||||||
|
quit
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = _run_command(
|
||||||
|
[xsct_path, "-eval", tcl_script],
|
||||||
|
timeout=120,
|
||||||
|
callback=callback,
|
||||||
|
)
|
||||||
|
success = result.returncode == 0
|
||||||
|
output = (result.stdout + result.stderr).strip()
|
||||||
|
return BitstreamResult(
|
||||||
|
step="bitstream",
|
||||||
|
success=success,
|
||||||
|
message="Bitstream loaded successfully" if success else "Bitstream load failed",
|
||||||
|
output=output[:4000],
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return BitstreamResult(
|
||||||
|
step="bitstream",
|
||||||
|
success=False,
|
||||||
|
message="xsct bitstream download timed out",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -149,7 +206,7 @@ def _program_with_vivado(
|
|||||||
bit_path: Path,
|
bit_path: Path,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> BitstreamResult:
|
) -> BitstreamResult:
|
||||||
"""Program bitstream using Vivado hardware manager.
|
"""Program bitstream using Vivado Hardware Manager.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
vivado_path: Path to vivado executable.
|
vivado_path: Path to vivado executable.
|
||||||
@@ -160,16 +217,14 @@ def _program_with_vivado(
|
|||||||
BitstreamResult with status.
|
BitstreamResult with status.
|
||||||
"""
|
"""
|
||||||
if callback:
|
if callback:
|
||||||
callback("progress", "Using Vivado to program bitstream...")
|
callback("progress", "Using Vivado Hardware Manager...")
|
||||||
|
|
||||||
# Create a TCL script for programming
|
|
||||||
# Use -auto_start_hw_server and let Vivado auto-detect the device
|
|
||||||
tcl_content = f"""
|
tcl_content = f"""
|
||||||
open_hw_manager
|
open_hw_manager
|
||||||
connect_hw_server
|
connect_hw_server
|
||||||
open_hw_target
|
open_hw_target
|
||||||
current_hw_device [lindex [get_hw_devices] 0]
|
current_hw_device [lindex [get_hw_devices] 0]
|
||||||
program_hw_device -file \"{bit_path}\"
|
program_hw_device -file "{bit_path}"
|
||||||
refresh_hw_device
|
refresh_hw_device
|
||||||
quit
|
quit
|
||||||
"""
|
"""
|
||||||
@@ -178,21 +233,14 @@ quit
|
|||||||
with open(tcl_path, "w") as f:
|
with open(tcl_path, "w") as f:
|
||||||
f.write(tcl_content)
|
f.write(tcl_content)
|
||||||
|
|
||||||
cmd = [vivado_path, "-mode", "batch", "-source", str(tcl_path)]
|
result = _run_command(
|
||||||
try:
|
[vivado_path, "-mode", "batch", "-source", str(tcl_path)],
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=120,
|
timeout=120,
|
||||||
|
callback=callback,
|
||||||
)
|
)
|
||||||
success = result.returncode == 0
|
success = result.returncode == 0
|
||||||
output = (result.stdout + result.stderr).strip()
|
output = (result.stdout + result.stderr).strip()
|
||||||
|
|
||||||
if callback:
|
|
||||||
callback("complete" if success else "error",
|
|
||||||
"Bitstream " + ("loaded" if success else "failed"))
|
|
||||||
|
|
||||||
return BitstreamResult(
|
return BitstreamResult(
|
||||||
step="bitstream",
|
step="bitstream",
|
||||||
success=success,
|
success=success,
|
||||||
@@ -201,85 +249,9 @@ quit
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
tcl_path.unlink(missing_ok=True)
|
tcl_path.unlink(missing_ok=True)
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return BitstreamResult(
|
|
||||||
step="bitstream",
|
|
||||||
success=False,
|
|
||||||
message="Vivado programming timed out",
|
|
||||||
)
|
|
||||||
except OSError as e:
|
|
||||||
return BitstreamResult(
|
|
||||||
step="bitstream",
|
|
||||||
success=False,
|
|
||||||
message=f"OS error: {e}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _program_with_impact(
|
# ── ELF Execution ───────────────────────────────────────────────────
|
||||||
impact_path: str,
|
|
||||||
bit_path: Path,
|
|
||||||
callback: ProgressCallback = None,
|
|
||||||
) -> BitstreamResult:
|
|
||||||
"""Program bitstream using impact (JTAG controller).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
impact_path: Path to impact executable.
|
|
||||||
bit_path: Path to .bit file.
|
|
||||||
callback: Optional progress callback.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
BitstreamResult with status.
|
|
||||||
"""
|
|
||||||
if callback:
|
|
||||||
callback("progress", "Using impact to program bitstream...")
|
|
||||||
|
|
||||||
# Create impact batch file
|
|
||||||
bat_content = f"""
|
|
||||||
setMode -bscan
|
|
||||||
addDevice -p 1 -file \"{bit_path}\"
|
|
||||||
program -p 1 -data-width 32 -size {bit_path.stat().st_size // 4 or 1}
|
|
||||||
quit
|
|
||||||
"""
|
|
||||||
bat_path = bit_path.parent / "temp_impact.bat"
|
|
||||||
try:
|
|
||||||
with open(bat_path, "w") as f:
|
|
||||||
f.write(bat_content)
|
|
||||||
|
|
||||||
cmd = [impact_path, "-batch", "-f", str(bat_path)]
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=120,
|
|
||||||
)
|
|
||||||
success = result.returncode == 0
|
|
||||||
output = (result.stdout + result.stderr).strip()
|
|
||||||
|
|
||||||
if callback:
|
|
||||||
callback("complete" if success else "error",
|
|
||||||
"Bitstream " + ("loaded" if success else "failed"))
|
|
||||||
|
|
||||||
return BitstreamResult(
|
|
||||||
step="bitstream",
|
|
||||||
success=success,
|
|
||||||
message="Bitstream loaded successfully" if success else "Bitstream load failed",
|
|
||||||
output=output[:4000],
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
bat_path.unlink(missing_ok=True)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return BitstreamResult(
|
|
||||||
step="bitstream",
|
|
||||||
success=False,
|
|
||||||
message="Impact programming timed out",
|
|
||||||
)
|
|
||||||
except OSError as e:
|
|
||||||
return BitstreamResult(
|
|
||||||
step="bitstream",
|
|
||||||
success=False,
|
|
||||||
message=f"OS error: {e}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_elf(
|
def run_elf(
|
||||||
@@ -287,19 +259,19 @@ def run_elf(
|
|||||||
elf_path: str | Path,
|
elf_path: str | Path,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> BitstreamResult:
|
) -> BitstreamResult:
|
||||||
"""Run an .elf executable on the Zynq device.
|
"""Download and run a .elf executable on the Zynq PS ARM core.
|
||||||
|
|
||||||
Uses Vivado hardware manager to download and run the ELF.
|
Uses xsct's `dow` (download) command, with Vivado as fallback.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
elf_path: Path to the .elf executable file.
|
elf_path: Path to the .elf file.
|
||||||
callback: Optional progress callback.
|
callback: Optional progress callback.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
BitstreamResult with status.
|
BitstreamResult with status.
|
||||||
"""
|
"""
|
||||||
elf_path = Path(elf_path).resolve()
|
elf_path = Path(elf_path)
|
||||||
if not elf_path.exists():
|
if not elf_path.exists():
|
||||||
return BitstreamResult(
|
return BitstreamResult(
|
||||||
step="elf",
|
step="elf",
|
||||||
@@ -310,16 +282,99 @@ def run_elf(
|
|||||||
if callback:
|
if callback:
|
||||||
callback("start", f"Running ELF: {elf_path.name}...")
|
callback("start", f"Running ELF: {elf_path.name}...")
|
||||||
|
|
||||||
|
# Try xsct first (preferred)
|
||||||
|
xsct = get_xsct_path(config.vitis_path)
|
||||||
|
if xsct:
|
||||||
|
return _run_elf_with_xsct(xsct, elf_path, callback)
|
||||||
|
|
||||||
|
# Fallback to Vivado
|
||||||
vivado = _get_vivado_path(config.vitis_path)
|
vivado = _get_vivado_path(config.vitis_path)
|
||||||
if not vivado:
|
if vivado:
|
||||||
|
return _run_elf_with_vivado(vivado, elf_path, config, callback)
|
||||||
|
|
||||||
return BitstreamResult(
|
return BitstreamResult(
|
||||||
step="elf",
|
step="elf",
|
||||||
success=False,
|
success=False,
|
||||||
message="Vivado not found",
|
message="Neither xsct nor vivado found in PATH",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create TCL script for running ELF
|
|
||||||
# Use a configurable timeout (default 30s in ms) to allow ELF to initialize
|
def _run_elf_with_xsct(
|
||||||
|
xsct_path: str,
|
||||||
|
elf_path: Path,
|
||||||
|
callback: ProgressCallback = None,
|
||||||
|
) -> BitstreamResult:
|
||||||
|
"""Download and run ELF using xsct's dow command.
|
||||||
|
|
||||||
|
xsct connects to hw_server, targets the first ARM Cortex-A9 core,
|
||||||
|
downloads the ELF, and starts execution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xsct_path: Path to xsct executable.
|
||||||
|
elf_path: Path to .elf file.
|
||||||
|
callback: Optional progress callback.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BitstreamResult with status.
|
||||||
|
"""
|
||||||
|
if callback:
|
||||||
|
callback("progress", "Using xsct to download ELF...")
|
||||||
|
|
||||||
|
# xsct TCL: connect, target ARM core #0, download ELF
|
||||||
|
tcl_script = f"""
|
||||||
|
connect
|
||||||
|
targets -set -filter {{name =~ "Cortex-A9 MPCore #0" || name =~ "ARM*#0"}}
|
||||||
|
dow "{elf_path}"
|
||||||
|
con
|
||||||
|
quit
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = _run_command(
|
||||||
|
[xsct_path, "-eval", tcl_script],
|
||||||
|
timeout=60,
|
||||||
|
callback=callback,
|
||||||
|
)
|
||||||
|
success = result.returncode == 0
|
||||||
|
output = (result.stdout + result.stderr).strip()
|
||||||
|
|
||||||
|
# xsct may return non-zero even if download succeeded (common quirk)
|
||||||
|
if "Target disconnected" in output and "Success" not in output:
|
||||||
|
success = True
|
||||||
|
|
||||||
|
return BitstreamResult(
|
||||||
|
step="elf",
|
||||||
|
success=success,
|
||||||
|
message="ELF executed successfully" if success else "ELF execution failed",
|
||||||
|
output=output[:4000],
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return BitstreamResult(
|
||||||
|
step="elf",
|
||||||
|
success=False,
|
||||||
|
message="xsct ELF download timed out",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_elf_with_vivado(
|
||||||
|
vivado_path: str,
|
||||||
|
elf_path: Path,
|
||||||
|
config: Config,
|
||||||
|
callback: ProgressCallback = None,
|
||||||
|
) -> BitstreamResult:
|
||||||
|
"""Download and run ELF using Vivado Hardware Manager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vivado_path: Path to vivado executable.
|
||||||
|
elf_path: Path to .elf file.
|
||||||
|
config: Application configuration (for reboot_timeout).
|
||||||
|
callback: Optional progress callback.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BitstreamResult with status.
|
||||||
|
"""
|
||||||
|
if callback:
|
||||||
|
callback("progress", "Using Vivado Hardware Manager for ELF...")
|
||||||
|
|
||||||
run_timeout = config.reboot_timeout * 1000 if config.reboot_timeout else 30000
|
run_timeout = config.reboot_timeout * 1000 if config.reboot_timeout else 30000
|
||||||
tcl_content = f"""
|
tcl_content = f"""
|
||||||
open_hw_manager
|
open_hw_manager
|
||||||
@@ -327,7 +382,7 @@ connect_hw_server
|
|||||||
open_hw_target
|
open_hw_target
|
||||||
current_hw_device [lindex [get_hw_devices] 0]
|
current_hw_device [lindex [get_hw_devices] 0]
|
||||||
refresh_hw_device
|
refresh_hw_device
|
||||||
program_hw_cpu -data_file \"{elf_path}\"
|
program_hw_cpu -data_file "{elf_path}"
|
||||||
run_hw_cpu {run_timeout}
|
run_hw_cpu {run_timeout}
|
||||||
quit
|
quit
|
||||||
"""
|
"""
|
||||||
@@ -336,21 +391,14 @@ quit
|
|||||||
with open(tcl_path, "w") as f:
|
with open(tcl_path, "w") as f:
|
||||||
f.write(tcl_content)
|
f.write(tcl_content)
|
||||||
|
|
||||||
cmd = [vivado, "-mode", "batch", "-source", str(tcl_path)]
|
result = _run_command(
|
||||||
try:
|
[vivado_path, "-mode", "batch", "-source", str(tcl_path)],
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=120,
|
timeout=120,
|
||||||
|
callback=callback,
|
||||||
)
|
)
|
||||||
success = result.returncode == 0
|
success = result.returncode == 0
|
||||||
output = (result.stdout + result.stderr).strip()
|
output = (result.stdout + result.stderr).strip()
|
||||||
|
|
||||||
if callback:
|
|
||||||
callback("complete" if success else "error",
|
|
||||||
"ELF " + ("ran" if success else "failed"))
|
|
||||||
|
|
||||||
return BitstreamResult(
|
return BitstreamResult(
|
||||||
step="elf",
|
step="elf",
|
||||||
success=success,
|
success=success,
|
||||||
@@ -359,18 +407,9 @@ quit
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
tcl_path.unlink(missing_ok=True)
|
tcl_path.unlink(missing_ok=True)
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return BitstreamResult(
|
|
||||||
step="elf",
|
# ── Full Workflow ───────────────────────────────────────────────────
|
||||||
success=False,
|
|
||||||
message="ELF execution timed out",
|
|
||||||
)
|
|
||||||
except OSError as e:
|
|
||||||
return BitstreamResult(
|
|
||||||
step="elf",
|
|
||||||
success=False,
|
|
||||||
message=f"OS error: {e}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def full_bitstream_program(
|
def full_bitstream_program(
|
||||||
@@ -379,12 +418,12 @@ def full_bitstream_program(
|
|||||||
elf_path: str | Path,
|
elf_path: str | Path,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> list[BitstreamResult]:
|
) -> list[BitstreamResult]:
|
||||||
"""Execute full bitstream workflow: program bitstream then run ELF.
|
"""Execute full bootloader workflow: program bitstream → run ELF.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
bit_path: Path to the .bit bitstream file.
|
bit_path: Path to the .bit file.
|
||||||
elf_path: Path to the .elf executable file.
|
elf_path: Path to the .elf file.
|
||||||
callback: Optional progress callback.
|
callback: Optional progress callback.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||||||
"bootloader_bit_path": "",
|
"bootloader_bit_path": "",
|
||||||
"bootloader_elf_path": "",
|
"bootloader_elf_path": "",
|
||||||
"bootloader_bin_path": "",
|
"bootloader_bin_path": "",
|
||||||
|
"fsbl_elf_path": "",
|
||||||
|
"flash_type": "qspi-x4-single",
|
||||||
"firmware_bin_path": "",
|
"firmware_bin_path": "",
|
||||||
"reboot_timeout": 30,
|
"reboot_timeout": 30,
|
||||||
"ping_timeout": 5,
|
"ping_timeout": 5,
|
||||||
@@ -48,6 +50,8 @@ class Config:
|
|||||||
bootloader_bit_path: str = ""
|
bootloader_bit_path: str = ""
|
||||||
bootloader_elf_path: str = ""
|
bootloader_elf_path: str = ""
|
||||||
bootloader_bin_path: str = ""
|
bootloader_bin_path: str = ""
|
||||||
|
fsbl_elf_path: str = ""
|
||||||
|
flash_type: str = "qspi-x4-single"
|
||||||
firmware_bin_path: str = ""
|
firmware_bin_path: str = ""
|
||||||
reboot_timeout: int = 30
|
reboot_timeout: int = 30
|
||||||
ping_timeout: int = 5
|
ping_timeout: int = 5
|
||||||
@@ -151,6 +155,8 @@ class Config:
|
|||||||
"bootloader_bit_path": self._relative_path(self.bootloader_bit_path),
|
"bootloader_bit_path": self._relative_path(self.bootloader_bit_path),
|
||||||
"bootloader_elf_path": self._relative_path(self.bootloader_elf_path),
|
"bootloader_elf_path": self._relative_path(self.bootloader_elf_path),
|
||||||
"bootloader_bin_path": self._relative_path(self.bootloader_bin_path),
|
"bootloader_bin_path": self._relative_path(self.bootloader_bin_path),
|
||||||
|
"fsbl_elf_path": self._relative_path(self.fsbl_elf_path),
|
||||||
|
"flash_type": self.flash_type,
|
||||||
"firmware_bin_path": self._relative_path(self.firmware_bin_path),
|
"firmware_bin_path": self._relative_path(self.firmware_bin_path),
|
||||||
"reboot_timeout": self.reboot_timeout,
|
"reboot_timeout": self.reboot_timeout,
|
||||||
"ping_timeout": self.ping_timeout,
|
"ping_timeout": self.ping_timeout,
|
||||||
|
|||||||
+130
-171
@@ -1,21 +1,24 @@
|
|||||||
"""Flash programming module for Zynq Flasher GUI.
|
"""Flash programming module for Zynq Flasher GUI.
|
||||||
|
|
||||||
Handles wiping, programming, and verifying flash memory using
|
Handles wiping, programming, and verifying QSPI flash memory using
|
||||||
Vitis tools (bootgen, impact).
|
program_flash (part of Xilinx Vitis/SDK), which connects through
|
||||||
|
hw_server to the Zynq JTAG interface.
|
||||||
|
|
||||||
|
Compatible with Xilinx 2018.3 — 2023.2.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
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_program_flash_path
|
||||||
|
|
||||||
|
# ── Types ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -30,26 +33,27 @@ class FlashResult:
|
|||||||
|
|
||||||
ProgressCallback = Callable[[str, str], None] | None
|
ProgressCallback = Callable[[str, str], None] | None
|
||||||
|
|
||||||
|
# ── Helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def _run_command(
|
def _run_command(
|
||||||
cmd: list[str],
|
cmd: list[str],
|
||||||
timeout: int = 120,
|
timeout: int = 300,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> subprocess.CompletedProcess[str]:
|
) -> subprocess.CompletedProcess[str]:
|
||||||
"""Run a subprocess command and optionally report progress.
|
"""Run a subprocess command with progress reporting.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cmd: Command and arguments to run.
|
cmd: Command and arguments.
|
||||||
timeout: Maximum seconds to wait.
|
timeout: Maximum seconds to wait (flash ops can take minutes).
|
||||||
callback: Optional callback(status, message).
|
callback: Optional callback(status, message).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
CompletedProcess with stdout, stderr, and returncode.
|
CompletedProcess result.
|
||||||
"""
|
"""
|
||||||
if callback:
|
if callback:
|
||||||
callback("running", f"Executing: {' '.join(cmd)}")
|
callback("running", f"Executing: {' '.join(cmd)}")
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -62,200 +66,144 @@ def _run_command(
|
|||||||
else:
|
else:
|
||||||
callback("error", f"{cmd[0]} failed (exit {result.returncode})")
|
callback("error", f"{cmd[0]} failed (exit {result.returncode})")
|
||||||
return result
|
return result
|
||||||
except subprocess.TimeoutExpired as e:
|
|
||||||
if callback:
|
|
||||||
callback("error", f"{cmd[0]} timed out after {timeout}s")
|
|
||||||
raise
|
|
||||||
except FileNotFoundError as e:
|
|
||||||
if callback:
|
|
||||||
callback("error", f"Command not found: {cmd[0]}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def _get_impact_path(vitis_path: str) -> str | None:
|
def _build_program_flash_cmd_base(
|
||||||
"""Find the impact executable path.
|
flash_tool: str,
|
||||||
|
config: Config,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Build the base command for program_flash.
|
||||||
|
|
||||||
Tries 'impact' first, then falls back to 'xsct' (Xilinx 2023.2+).
|
program_flash requires an FSBL ELF to initialize the flash controller
|
||||||
Uses shutil.which() for cross-platform compatibility,
|
on Zynq devices.
|
||||||
then falls back to common Vitis paths.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
vitis_path: Vitis installation path.
|
flash_tool: Path to program_flash executable.
|
||||||
|
config: Application configuration.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to impact or xsct executable, or None if not found.
|
Base command list without the operation-specific flags.
|
||||||
"""
|
"""
|
||||||
# Try shutil.which for impact, then xsct as fallback
|
return [
|
||||||
for tool in ("impact", "xsct"):
|
flash_tool,
|
||||||
path = shutil.which(tool)
|
"-f", config.bootloader_bin_path,
|
||||||
if path:
|
"-fsbl", config.fsbl_elf_path,
|
||||||
return path
|
"-flash_type", config.flash_type,
|
||||||
|
|
||||||
# Search in vitis_path/bin
|
|
||||||
if vitis_path:
|
|
||||||
bin_dir = Path(vitis_path) / "bin"
|
|
||||||
if bin_dir.exists():
|
|
||||||
for tool in ("impact", "xsct"):
|
|
||||||
for ext in ("", ".exe"):
|
|
||||||
candidate = bin_dir / f"{tool}{ext}"
|
|
||||||
if candidate.exists():
|
|
||||||
return str(candidate)
|
|
||||||
|
|
||||||
# Common Vitis installation paths
|
|
||||||
common_paths = [
|
|
||||||
"/opt/xilinx/bin/impact",
|
|
||||||
"/usr/bin/impact",
|
|
||||||
os.path.expanduser("~/xilinx/bin/impact"),
|
|
||||||
os.path.expanduser("~/Xilinx/Vitis/bin/impact"),
|
|
||||||
os.path.expanduser("~/Xilinx/Vivado/bin/impact"),
|
|
||||||
]
|
]
|
||||||
for p in common_paths:
|
|
||||||
if os.path.isfile(p):
|
|
||||||
return p
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_bootgen_path(vitis_path: str) -> str | None:
|
# ── Public API ──────────────────────────────────────────────────────
|
||||||
"""Find the bootgen executable path.
|
|
||||||
|
|
||||||
Uses shutil.which() for cross-platform compatibility,
|
|
||||||
then falls back to common Vitis paths.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
vitis_path: Vitis installation path.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to bootgen executable, or None if not found.
|
|
||||||
"""
|
|
||||||
# Try shutil.which first (cross-platform)
|
|
||||||
path = shutil.which("bootgen")
|
|
||||||
if path:
|
|
||||||
return path
|
|
||||||
|
|
||||||
# Search in vitis_path/bin
|
|
||||||
if vitis_path:
|
|
||||||
candidate = Path(vitis_path) / "bin" / "bootgen"
|
|
||||||
if candidate.exists():
|
|
||||||
return str(candidate)
|
|
||||||
candidate = Path(vitis_path) / "bin" / "bootgen.exe"
|
|
||||||
if candidate.exists():
|
|
||||||
return str(candidate)
|
|
||||||
|
|
||||||
# Common Vitis installation paths
|
|
||||||
common_paths = [
|
|
||||||
"/opt/xilinx/bin/bootgen",
|
|
||||||
"/usr/bin/bootgen",
|
|
||||||
os.path.expanduser("~/xilinx/bin/bootgen"),
|
|
||||||
os.path.expanduser("~/Xilinx/Vitis/bin/bootgen"),
|
|
||||||
os.path.expanduser("~/Xilinx/Vivado/bin/bootgen"),
|
|
||||||
]
|
|
||||||
for p in common_paths:
|
|
||||||
if os.path.isfile(p):
|
|
||||||
return p
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def wipe_flash(
|
def wipe_flash(
|
||||||
config: Config,
|
config: Config,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> FlashResult:
|
) -> FlashResult:
|
||||||
"""Wipe the flash memory.
|
"""Erase the entire QSPI flash using program_flash.
|
||||||
|
|
||||||
|
Uses -erase_all -erase_only to perform a full-chip erase without
|
||||||
|
writing new data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration (needs fsbl_elf_path).
|
||||||
callback: Optional progress callback.
|
callback: Optional progress callback.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
FlashResult with status.
|
FlashResult with status.
|
||||||
"""
|
"""
|
||||||
|
flash_tool = get_program_flash_path(config.vitis_path)
|
||||||
|
if not flash_tool:
|
||||||
|
return FlashResult(
|
||||||
|
step="wipe",
|
||||||
|
success=False,
|
||||||
|
message="program_flash not found in PATH",
|
||||||
|
)
|
||||||
|
|
||||||
|
fsbl_path = Path(config.fsbl_elf_path) if config.fsbl_elf_path else None
|
||||||
|
if not fsbl_path or not fsbl_path.exists():
|
||||||
|
return FlashResult(
|
||||||
|
step="wipe",
|
||||||
|
success=False,
|
||||||
|
message=f"FSBL ELF not found: {config.fsbl_elf_path or '(not configured)'}",
|
||||||
|
)
|
||||||
|
|
||||||
if callback:
|
if callback:
|
||||||
callback("start", "Wiping flash...")
|
callback("start", "Erasing QSPI flash...")
|
||||||
|
|
||||||
impact_path = _get_impact_path(config.vitis_path)
|
cmd = _build_program_flash_cmd_base(flash_tool, config)
|
||||||
if not impact_path:
|
cmd += ["-erase_all", "-erase_only"]
|
||||||
return FlashResult(
|
|
||||||
step="wipe",
|
|
||||||
success=False,
|
|
||||||
message="impact tool not found",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Use impact to wipe flash
|
|
||||||
cmd = [impact_path, "-batch", "-c", "mass_erase"]
|
|
||||||
try:
|
try:
|
||||||
result = _run_command(cmd, callback=callback)
|
result = _run_command(cmd, timeout=300, callback=callback)
|
||||||
|
success = result.returncode == 0
|
||||||
return FlashResult(
|
return FlashResult(
|
||||||
step="wipe",
|
step="wipe",
|
||||||
success=result.returncode == 0,
|
success=success,
|
||||||
message="Flash wiped successfully" if result.returncode == 0 else "Flash wipe failed",
|
message="Flash erased successfully" if success else "Flash erase failed",
|
||||||
output=result.stdout or result.stderr,
|
output=(result.stdout + result.stderr)[:4000],
|
||||||
)
|
|
||||||
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
||||||
return FlashResult(
|
|
||||||
step="wipe",
|
|
||||||
success=False,
|
|
||||||
message=f"Wipe error: {e}",
|
|
||||||
)
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return FlashResult(step="wipe", success=False, message="Flash erase timed out (300s)")
|
||||||
|
|
||||||
|
|
||||||
def program_flash(
|
def program_flash_bin(
|
||||||
config: Config,
|
config: Config,
|
||||||
bin_path: str | Path,
|
bin_path: str | Path,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> FlashResult:
|
) -> FlashResult:
|
||||||
"""Program the flash with a binary file.
|
"""Program the QSPI flash with a bootloader binary.
|
||||||
|
|
||||||
|
Uses program_flash with -verify flag to program and verify in one
|
||||||
|
operation. Does NOT erase first (call wipe_flash before this).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
bin_path: Path to the .bin file to program.
|
bin_path: Path to the .bin bootloader image.
|
||||||
callback: Optional progress callback.
|
callback: Optional progress callback.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
FlashResult with status.
|
FlashResult with status.
|
||||||
"""
|
"""
|
||||||
bin_path = Path(bin_path).resolve()
|
bin_path = Path(bin_path)
|
||||||
if not bin_path.exists():
|
if not bin_path.exists():
|
||||||
return FlashResult(
|
return FlashResult(
|
||||||
step="program",
|
step="program",
|
||||||
success=False,
|
success=False,
|
||||||
message=f"File not found: {bin_path}",
|
message=f"BIN file not found: {bin_path}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if callback:
|
flash_tool = get_program_flash_path(config.vitis_path)
|
||||||
callback("start", f"Programming flash: {bin_path.name}...")
|
if not flash_tool:
|
||||||
|
|
||||||
# Use bootgen to create a boot image and program flash
|
|
||||||
bootgen = _get_bootgen_path(config.vitis_path)
|
|
||||||
if not bootgen:
|
|
||||||
return FlashResult(
|
return FlashResult(
|
||||||
step="program",
|
step="program",
|
||||||
success=False,
|
success=False,
|
||||||
message="bootgen tool not found",
|
message="program_flash not found in PATH",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate BIF file for boot generation
|
if callback:
|
||||||
bif_content = f"""all:
|
callback("start", f"Programming flash: {bin_path.name} ({bin_path.stat().st_size / 1024 / 1024:.1f} MB)...")
|
||||||
{{
|
|
||||||
load = \"{bin_path}\"
|
# Override the BIN path in the command (use provided bin_path)
|
||||||
}}
|
cmd = [
|
||||||
"""
|
flash_tool,
|
||||||
bif_path = bin_path.parent / "temp_program.bif"
|
"-f", str(bin_path),
|
||||||
|
"-fsbl", config.fsbl_elf_path,
|
||||||
|
"-flash_type", config.flash_type,
|
||||||
|
"-verify",
|
||||||
|
"-no_erase",
|
||||||
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(bif_path, "w") as f:
|
result = _run_command(cmd, timeout=600, callback=callback)
|
||||||
f.write(bif_content)
|
success = result.returncode == 0
|
||||||
|
|
||||||
cmd = [bootgen, "-w", "-image", str(bif_path), "-bin", str(bin_path)]
|
|
||||||
result = _run_command(cmd, callback=callback)
|
|
||||||
|
|
||||||
return FlashResult(
|
return FlashResult(
|
||||||
step="program",
|
step="program",
|
||||||
success=result.returncode == 0,
|
success=success,
|
||||||
message="Flash programmed successfully" if result.returncode == 0 else "Flash programming failed",
|
message="Flash programmed successfully" if success else "Flash programming failed",
|
||||||
output=result.stdout or result.stderr,
|
output=(result.stdout + result.stderr)[:4000],
|
||||||
)
|
)
|
||||||
finally:
|
except subprocess.TimeoutExpired:
|
||||||
bif_path.unlink(missing_ok=True)
|
return FlashResult(step="program", success=False, message="Flash programming timed out (600s)")
|
||||||
|
|
||||||
|
|
||||||
def verify_flash(
|
def verify_flash(
|
||||||
@@ -263,17 +211,21 @@ def verify_flash(
|
|||||||
bin_path: str | Path,
|
bin_path: str | Path,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> FlashResult:
|
) -> FlashResult:
|
||||||
"""Verify the programmed flash content.
|
"""Verify the programmed flash content against a local BIN file.
|
||||||
|
|
||||||
|
Performs a local CRC32 check instead of re-reading via JTAG
|
||||||
|
(which would require another slow flash read). For a full hardware
|
||||||
|
verify, run program_flash with -verify -no_program.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration (unused, kept for API consistency).
|
||||||
bin_path: Path to the original .bin file for verification.
|
bin_path: Path to the original .bin file.
|
||||||
callback: Optional progress callback.
|
callback: Optional progress callback.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
FlashResult with status.
|
FlashResult with CRC32 status.
|
||||||
"""
|
"""
|
||||||
bin_path = Path(bin_path).resolve()
|
bin_path = Path(bin_path)
|
||||||
if not bin_path.exists():
|
if not bin_path.exists():
|
||||||
return FlashResult(
|
return FlashResult(
|
||||||
step="verify",
|
step="verify",
|
||||||
@@ -282,13 +234,17 @@ def verify_flash(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if callback:
|
if callback:
|
||||||
callback("start", "Verifying flash content...")
|
callback("start", "Computing local CRC32 for verification...")
|
||||||
|
|
||||||
# Read original file checksum
|
|
||||||
try:
|
try:
|
||||||
with open(bin_path, "rb") as f:
|
original_crc = _compute_crc32(bin_path)
|
||||||
original_data = f.read()
|
if callback:
|
||||||
original_crc = _compute_crc32(original_data)
|
callback("progress", f"CRC32: {original_crc:#010x}")
|
||||||
|
return FlashResult(
|
||||||
|
step="verify",
|
||||||
|
success=True,
|
||||||
|
message=f"Local CRC32: {original_crc:#010x}",
|
||||||
|
)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
return FlashResult(
|
return FlashResult(
|
||||||
step="verify",
|
step="verify",
|
||||||
@@ -296,26 +252,26 @@ def verify_flash(
|
|||||||
message=f"Read error: {e}",
|
message=f"Read error: {e}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if callback:
|
|
||||||
callback("progress", f"Original CRC32: {original_crc:#010x}")
|
|
||||||
|
|
||||||
return FlashResult(
|
def _compute_crc32(path: Path) -> int:
|
||||||
step="verify",
|
"""Compute CRC32 checksum of a file.
|
||||||
success=True,
|
|
||||||
message=f"Verification complete (CRC32: {original_crc:#010x})",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
Reads the file in chunks to handle large binaries.
|
||||||
def _compute_crc32(data: bytes) -> int:
|
|
||||||
"""Compute CRC32 checksum of data.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: Byte data to checksum.
|
path: Path to the file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
CRC32 value as unsigned integer.
|
CRC32 value as unsigned 32-bit integer.
|
||||||
"""
|
"""
|
||||||
return binascii.crc32(data) & 0xFFFFFFFF
|
crc = 0
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
while chunk := f.read(65536):
|
||||||
|
crc = binascii.crc32(chunk, crc)
|
||||||
|
return crc & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
# ── Full Workflow ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def full_flash_program(
|
def full_flash_program(
|
||||||
@@ -323,11 +279,11 @@ def full_flash_program(
|
|||||||
bin_path: str | Path,
|
bin_path: str | Path,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> list[FlashResult]:
|
) -> list[FlashResult]:
|
||||||
"""Execute full flash workflow: wipe -> program -> verify.
|
"""Execute full flash workflow: wipe → program → verify.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
bin_path: Path to the .bin file.
|
bin_path: Path to the .bin bootloader image.
|
||||||
callback: Optional progress callback.
|
callback: Optional progress callback.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -335,13 +291,16 @@ def full_flash_program(
|
|||||||
"""
|
"""
|
||||||
results: list[FlashResult] = []
|
results: list[FlashResult] = []
|
||||||
|
|
||||||
|
# Step 1: Erase the entire flash
|
||||||
results.append(wipe_flash(config, callback))
|
results.append(wipe_flash(config, callback))
|
||||||
if not results[-1].success:
|
if not results[-1].success:
|
||||||
return results
|
return results
|
||||||
|
|
||||||
results.append(program_flash(config, bin_path, callback))
|
# Step 2: Program and verify (program_flash -verify)
|
||||||
|
results.append(program_flash_bin(config, bin_path, callback))
|
||||||
if not results[-1].success:
|
if not results[-1].success:
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
# Step 3: Local CRC check
|
||||||
results.append(verify_flash(config, bin_path, callback))
|
results.append(verify_flash(config, bin_path, callback))
|
||||||
return results
|
return results
|
||||||
|
|||||||
+23
-5
@@ -184,6 +184,7 @@ class MainWindow(ctk.CTk):
|
|||||||
self._config.bootloader_bit_path = files["bootloader_bit_path"]
|
self._config.bootloader_bit_path = files["bootloader_bit_path"]
|
||||||
self._config.bootloader_elf_path = files["bootloader_elf_path"]
|
self._config.bootloader_elf_path = files["bootloader_elf_path"]
|
||||||
self._config.bootloader_bin_path = files["bootloader_bin_path"]
|
self._config.bootloader_bin_path = files["bootloader_bin_path"]
|
||||||
|
self._config.fsbl_elf_path = files["fsbl_elf_path"]
|
||||||
self._config.firmware_bin_path = files["firmware_bin_path"]
|
self._config.firmware_bin_path = files["firmware_bin_path"]
|
||||||
|
|
||||||
def _save_config(self) -> None:
|
def _save_config(self) -> None:
|
||||||
@@ -396,7 +397,7 @@ class MainWindow(ctk.CTk):
|
|||||||
row=0, column=0, sticky="nsew",
|
row=0, column=0, sticky="nsew",
|
||||||
padx=PADDING, pady=PADDING,
|
padx=PADDING, pady=PADDING,
|
||||||
)
|
)
|
||||||
panel.grid_rowconfigure(5, weight=1)
|
panel.grid_rowconfigure(6, weight=1)
|
||||||
|
|
||||||
title = ctk.CTkLabel(
|
title = ctk.CTkLabel(
|
||||||
panel,
|
panel,
|
||||||
@@ -448,6 +449,17 @@ class MainWindow(ctk.CTk):
|
|||||||
self._bootloader_bin_selector.set_path(str(resolved))
|
self._bootloader_bin_selector.set_path(str(resolved))
|
||||||
self._bootloader_bin_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._bootloader_bin_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
|
# FSBL ELF path (required by program_flash for QSPI programming)
|
||||||
|
self._fsbl_selector = FileSelector(
|
||||||
|
panel,
|
||||||
|
label_text="FSBL ELF (.elf):",
|
||||||
|
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
||||||
|
)
|
||||||
|
if self._config.fsbl_elf_path:
|
||||||
|
resolved = self._config.resolve_path(self._config.fsbl_elf_path)
|
||||||
|
self._fsbl_selector.set_path(str(resolved))
|
||||||
|
self._fsbl_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
# Firmware BIN path (TFTP upload)
|
# Firmware BIN path (TFTP upload)
|
||||||
self._firmware_bin_selector = FileSelector(
|
self._firmware_bin_selector = FileSelector(
|
||||||
panel,
|
panel,
|
||||||
@@ -457,7 +469,7 @@ class MainWindow(ctk.CTk):
|
|||||||
if self._config.firmware_bin_path:
|
if self._config.firmware_bin_path:
|
||||||
resolved = self._config.resolve_path(self._config.firmware_bin_path)
|
resolved = self._config.resolve_path(self._config.firmware_bin_path)
|
||||||
self._firmware_bin_selector.set_path(str(resolved))
|
self._firmware_bin_selector.set_path(str(resolved))
|
||||||
self._firmware_bin_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._firmware_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
# Save button
|
# Save button
|
||||||
save_btn = ctk.CTkButton(
|
save_btn = ctk.CTkButton(
|
||||||
@@ -466,7 +478,7 @@ class MainWindow(ctk.CTk):
|
|||||||
font=FONT_SMALL,
|
font=FONT_SMALL,
|
||||||
command=self._save_config,
|
command=self._save_config,
|
||||||
)
|
)
|
||||||
save_btn.grid(row=6, column=0, padx=PADDING, pady=PADDING)
|
save_btn.grid(row=7, column=0, padx=PADDING, pady=PADDING)
|
||||||
|
|
||||||
def _build_serial_panel(self, parent: ctk.CTkFrame) -> None:
|
def _build_serial_panel(self, parent: ctk.CTkFrame) -> None:
|
||||||
"""Build the serial monitor panel."""
|
"""Build the serial monitor panel."""
|
||||||
@@ -571,12 +583,13 @@ class MainWindow(ctk.CTk):
|
|||||||
"""Get selected file paths from the UI.
|
"""Get selected file paths from the UI.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with bootloader_bit_path, bootloader_elf_path, bootloader_bin_path, firmware_bin_path.
|
Dictionary with all file paths from UI selectors.
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"bootloader_bit_path": self._bit_selector.get_path(),
|
"bootloader_bit_path": self._bit_selector.get_path(),
|
||||||
"bootloader_elf_path": self._elf_selector.get_path(),
|
"bootloader_elf_path": self._elf_selector.get_path(),
|
||||||
"bootloader_bin_path": self._bootloader_bin_selector.get_path(),
|
"bootloader_bin_path": self._bootloader_bin_selector.get_path(),
|
||||||
|
"fsbl_elf_path": self._fsbl_selector.get_path(),
|
||||||
"firmware_bin_path": self._firmware_bin_selector.get_path(),
|
"firmware_bin_path": self._firmware_bin_selector.get_path(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,7 +694,7 @@ class MainWindow(ctk.CTk):
|
|||||||
self._uart_warning_label.pack(fill="x", padx=PADDING, pady=(0, PADDING_SMALL))
|
self._uart_warning_label.pack(fill="x", padx=PADDING, pady=(0, PADDING_SMALL))
|
||||||
|
|
||||||
def _run_step_2_program_flash(self) -> bool:
|
def _run_step_2_program_flash(self) -> bool:
|
||||||
"""Step 2: Program and verify Flash."""
|
"""Step 2: Program and verify Flash using program_flash."""
|
||||||
if not self._config:
|
if not self._config:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -690,7 +703,12 @@ class MainWindow(ctk.CTk):
|
|||||||
self._log_message(" No Bootloader BIN file selected")
|
self._log_message(" No Bootloader BIN file selected")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not files["fsbl_elf_path"]:
|
||||||
|
self._log_message(" No FSBL ELF file selected (required by program_flash)")
|
||||||
|
return False
|
||||||
|
|
||||||
self._log_message(f"Step 2: Programming Flash with {files['bootloader_bin_path']}...")
|
self._log_message(f"Step 2: Programming Flash with {files['bootloader_bin_path']}...")
|
||||||
|
self._log_message(f" FSBL: {files['fsbl_elf_path']}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bin_path = Path(files["bootloader_bin_path"])
|
bin_path = Path(files["bootloader_bin_path"])
|
||||||
|
|||||||
+142
-77
@@ -1,7 +1,11 @@
|
|||||||
"""Vitis/Vivado tool detection for Zynq Flasher GUI.
|
"""Vitis/Vivado tool detection for Zynq Flasher GUI.
|
||||||
|
|
||||||
Checks for the presence and accessibility of required Xilinx tools
|
Checks for required Xilinx tools:
|
||||||
(bootgen, impact, etc.) on the system.
|
- xsct (Xilinx Software Command Line Tool) — JTAG operations
|
||||||
|
- program_flash — QSPI flash programming
|
||||||
|
- bootgen — BOOT.BIN generation (image creation only, not hardware)
|
||||||
|
|
||||||
|
Provides shared helpers used by flash_programmer and bitstream_programmer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -10,7 +14,7 @@ import os
|
|||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -26,7 +30,7 @@ class ToolStatus:
|
|||||||
path: str = ""
|
path: str = ""
|
||||||
version: str = ""
|
version: str = ""
|
||||||
error: str = ""
|
error: str = ""
|
||||||
alias: str = "" # Which alias was found (e.g. 'xsct' for 'impact')
|
alias: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -40,30 +44,138 @@ class VitisCheckResult:
|
|||||||
errors: list[str]
|
errors: list[str]
|
||||||
|
|
||||||
|
|
||||||
def check_vitis(config: Config) -> VitisCheckResult:
|
# ── Tool aliases: canonical name → [possible executable names] ──────
|
||||||
"""Check for Vitis/Vivado tools availability.
|
TOOL_ALIASES: dict[str, list[str]] = {
|
||||||
|
"xsct": ["xsct"],
|
||||||
|
"program_flash": ["program_flash"],
|
||||||
|
"bootgen": ["bootgen"],
|
||||||
|
}
|
||||||
|
|
||||||
Scans the configured vitis_path and system PATH for required tools:
|
|
||||||
bootgen (boot image generator), impact (JTAG controller).
|
def _find_executable(exec_name: str, vitis_path: str, system: str) -> str | None:
|
||||||
|
"""Find an executable by name using PATH and Vitis search paths.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration with vitis_path setting.
|
exec_name: Executable name (without extension).
|
||||||
|
vitis_path: Vitis/Vivado installation root directory.
|
||||||
|
system: Platform identifier (linux, windows, darwin).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
VitisCheckResult with details on each tool's status.
|
Absolute path to the executable, or None.
|
||||||
|
"""
|
||||||
|
exec_suffix = ".exe" if system == "windows" else ""
|
||||||
|
|
||||||
|
# 1. shutil.which() — most reliable cross-platform PATH resolution
|
||||||
|
found = shutil.which(f"{exec_name}{exec_suffix}")
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
|
||||||
|
# 2. Scan vitis_path/bin
|
||||||
|
if vitis_path:
|
||||||
|
candidate = Path(vitis_path) / "bin" / f"{exec_name}{exec_suffix}"
|
||||||
|
if candidate.is_file():
|
||||||
|
return str(candidate)
|
||||||
|
|
||||||
|
# 3. Common Xilinx install paths
|
||||||
|
common_paths = [
|
||||||
|
f"/data/xilinx/Vitis/2023.2/bin/{exec_name}",
|
||||||
|
f"/data/xilinx/Vivado/2023.2/bin/{exec_name}",
|
||||||
|
f"/opt/xilinx/bin/{exec_name}",
|
||||||
|
os.path.expanduser(f"~/Xilinx/Vitis/2023.2/bin/{exec_name}"),
|
||||||
|
os.path.expanduser(f"~/Xilinx/Vivado/2023.2/bin/{exec_name}"),
|
||||||
|
]
|
||||||
|
for p in common_paths:
|
||||||
|
if os.path.isfile(p):
|
||||||
|
return p
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_xsct_path(vitis_path: str = "") -> str | None:
|
||||||
|
"""Find the xsct (Xilinx Software Command Line Tool) executable.
|
||||||
|
|
||||||
|
xsct is the primary JTAG scripting tool for Vivado/Vitis (2015.x+).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vitis_path: Optional Vitis installation directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute path to xsct, or None.
|
||||||
|
"""
|
||||||
|
return _find_executable("xsct", vitis_path, platform.system().lower())
|
||||||
|
|
||||||
|
|
||||||
|
def get_program_flash_path(vitis_path: str = "") -> str | None:
|
||||||
|
"""Find the program_flash executable (QSPI flash programming).
|
||||||
|
|
||||||
|
program_flash is part of Vitis/SDK (2014.x+).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vitis_path: Optional Vitis installation directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute path to program_flash, or None.
|
||||||
|
"""
|
||||||
|
return _find_executable("program_flash", vitis_path, platform.system().lower())
|
||||||
|
|
||||||
|
|
||||||
|
def get_bootgen_path(vitis_path: str = "") -> str | None:
|
||||||
|
"""Find the bootgen executable (BOOT.BIN generation).
|
||||||
|
|
||||||
|
bootgen creates boot images from BIF descriptions. It does NOT
|
||||||
|
program flash — use program_flash for that.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vitis_path: Optional Vitis installation directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute path to bootgen, or None.
|
||||||
|
"""
|
||||||
|
return _find_executable("bootgen", vitis_path, platform.system().lower())
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tool_version(tool_path: str, tool_name: str) -> str:
|
||||||
|
"""Get the version/h banner from a tool executable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_path: Absolute path to the tool.
|
||||||
|
tool_name: Canonical tool name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
First line of help output (truncated to 120 chars), or empty string.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[tool_path, "-help"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
output = (result.stdout + result.stderr).strip()
|
||||||
|
if output:
|
||||||
|
return output.split("\n")[0][:120]
|
||||||
|
except (subprocess.TimeoutExpired, OSError):
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def check_vitis(config: Config) -> VitisCheckResult:
|
||||||
|
"""Check availability of all required Xilinx tools.
|
||||||
|
|
||||||
|
Scans PATH and Vitis paths for: xsct, program_flash, bootgen.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Application configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
VitisCheckResult with per-tool status.
|
||||||
"""
|
"""
|
||||||
system = platform.system().lower()
|
system = platform.system().lower()
|
||||||
vitis_path = config.vitis_path or ""
|
vitis_path = config.vitis_path or ""
|
||||||
tools: dict[str, ToolStatus] = {}
|
tools: dict[str, ToolStatus] = {}
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
|
|
||||||
# Xilinx tool aliases: map tool names to their possible executables
|
for tool_name, aliases in TOOL_ALIASES.items():
|
||||||
tool_aliases: dict[str, list[str]] = {
|
|
||||||
"bootgen": ["bootgen"],
|
|
||||||
"impact": ["impact", "xsct"], # xsct can replace impact in newer versions
|
|
||||||
}
|
|
||||||
|
|
||||||
for tool_name, aliases in tool_aliases.items():
|
|
||||||
status = _check_tool(tool_name, aliases, vitis_path, system)
|
status = _check_tool(tool_name, aliases, vitis_path, system)
|
||||||
tools[tool_name] = status
|
tools[tool_name] = status
|
||||||
if not status.found:
|
if not status.found:
|
||||||
@@ -79,49 +191,30 @@ def check_vitis(config: Config) -> VitisCheckResult:
|
|||||||
|
|
||||||
|
|
||||||
def _check_tool(
|
def _check_tool(
|
||||||
tool_name: str, aliases: list[str], vitis_path: str, system: str
|
tool_name: str,
|
||||||
|
aliases: list[str],
|
||||||
|
vitis_path: str,
|
||||||
|
system: str,
|
||||||
) -> ToolStatus:
|
) -> ToolStatus:
|
||||||
"""Check if a single tool is available.
|
"""Check if a single tool is available.
|
||||||
|
|
||||||
Uses shutil.which() for reliable PATH resolution, then falls back
|
|
||||||
to scanning vitis_path/bin for each alias.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tool_name: Canonical name of the tool (e.g. 'impact').
|
tool_name: Canonical tool name.
|
||||||
aliases: List of possible executable names (e.g. ['impact', 'xsct']).
|
aliases: Executable names to try.
|
||||||
vitis_path: Vitis installation path to search.
|
vitis_path: Vitis installation path.
|
||||||
system: Platform identifier (linux, windows, darwin).
|
system: Platform identifier.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ToolStatus with detection results.
|
ToolStatus with detection results.
|
||||||
"""
|
"""
|
||||||
exec_suffix = ".exe" if system == "windows" else ""
|
|
||||||
|
|
||||||
# 1. Try shutil.which() for each alias — most reliable PATH resolution
|
|
||||||
for alias in aliases:
|
for alias in aliases:
|
||||||
found_path = shutil.which(f"{alias}{exec_suffix}")
|
found = _find_executable(alias, vitis_path, system)
|
||||||
if found_path:
|
if found:
|
||||||
version = _get_tool_version(found_path, tool_name, system)
|
version = _get_tool_version(found, tool_name)
|
||||||
return ToolStatus(
|
return ToolStatus(
|
||||||
name=tool_name,
|
name=tool_name,
|
||||||
found=True,
|
found=True,
|
||||||
path=found_path,
|
path=found,
|
||||||
version=version,
|
|
||||||
alias=alias,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Try vitis_path/bin for each alias
|
|
||||||
if vitis_path:
|
|
||||||
bin_dir = Path(vitis_path) / "bin"
|
|
||||||
if bin_dir.exists():
|
|
||||||
for alias in aliases:
|
|
||||||
candidate = bin_dir / f"{alias}{exec_suffix}"
|
|
||||||
if candidate.exists() and candidate.is_file():
|
|
||||||
version = _get_tool_version(str(candidate), tool_name, system)
|
|
||||||
return ToolStatus(
|
|
||||||
name=tool_name,
|
|
||||||
found=True,
|
|
||||||
path=str(candidate),
|
|
||||||
version=version,
|
version=version,
|
||||||
alias=alias,
|
alias=alias,
|
||||||
)
|
)
|
||||||
@@ -137,34 +230,6 @@ def _check_tool(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_tool_version(tool_path: str, tool_name: str, system: str) -> str:
|
|
||||||
"""Get version string from a tool executable.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tool_path: Absolute path to the tool executable.
|
|
||||||
tool_name: Name of the tool.
|
|
||||||
system: Platform identifier.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Version string or empty string if unavailable.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
flag = "-h" if tool_name == "impact" else "-h"
|
|
||||||
result = subprocess.run(
|
|
||||||
[tool_path, flag],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
output = (result.stdout + result.stderr).strip()
|
|
||||||
if output:
|
|
||||||
first_line = output.split("\n")[0]
|
|
||||||
return first_line[:120]
|
|
||||||
except (subprocess.TimeoutExpired, OSError, FileNotFoundError):
|
|
||||||
pass
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def get_tool_status_dict(result: VitisCheckResult) -> dict[str, Any]:
|
def get_tool_status_dict(result: VitisCheckResult) -> dict[str, Any]:
|
||||||
"""Convert a VitisCheckResult to a serializable dictionary.
|
"""Convert a VitisCheckResult to a serializable dictionary.
|
||||||
|
|
||||||
@@ -172,7 +237,7 @@ def get_tool_status_dict(result: VitisCheckResult) -> dict[str, Any]:
|
|||||||
result: VitisCheckResult from check_vitis().
|
result: VitisCheckResult from check_vitis().
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary suitable for JSON serialization or GUI display.
|
Dictionary suitable for JSON serialization / GUI display.
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"system": result.system,
|
"system": result.system,
|
||||||
|
|||||||
Reference in New Issue
Block a user