♻️ 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:
+249
-210
@@ -1,13 +1,16 @@
|
||||
"""Bitstream programming module for Zynq Flasher GUI.
|
||||
|
||||
Handles programming the Zynq device with a .bit bitstream file
|
||||
and running an .elf executable afterward.
|
||||
Downloads a .bit bitstream to the Zynq PL fabric and runs a .elf
|
||||
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
|
||||
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
@@ -15,11 +18,14 @@ from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from config_manager import Config
|
||||
from vitis_checker import get_xsct_path
|
||||
|
||||
# ── Types ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class BitstreamResult:
|
||||
"""Result of a bitstream programming operation."""
|
||||
"""Result of a bitstream programming or ELF execution operation."""
|
||||
|
||||
step: str
|
||||
success: bool
|
||||
@@ -29,93 +35,95 @@ class BitstreamResult:
|
||||
|
||||
ProgressCallback = Callable[[str, str], None] | None
|
||||
|
||||
|
||||
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
|
||||
# ── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
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,
|
||||
then falls back to common Vitis paths.
|
||||
Uses shutil.which() for PATH resolution, then common install paths.
|
||||
|
||||
Args:
|
||||
vitis_path: Vitis installation path.
|
||||
vitis_path: Optional Vitis installation directory.
|
||||
|
||||
Returns:
|
||||
Path to Vivado executable, or None if not found.
|
||||
Path to vivado executable, or None.
|
||||
"""
|
||||
path = shutil.which("vivado")
|
||||
if path:
|
||||
return path
|
||||
found = shutil.which("vivado")
|
||||
if found:
|
||||
return found
|
||||
if vitis_path:
|
||||
candidate = Path(vitis_path) / "bin" / "vivado"
|
||||
if candidate.exists():
|
||||
if candidate.is_file():
|
||||
return str(candidate)
|
||||
candidate = Path(vitis_path) / "bin" / "vivado.exe"
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
common_paths = [
|
||||
common = [
|
||||
"/data/xilinx/Vivado/2023.2/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):
|
||||
return p
|
||||
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(
|
||||
config: Config,
|
||||
bit_path: str | Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> 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:
|
||||
config: Application configuration.
|
||||
bit_path: Path to the .bit bitstream file.
|
||||
bit_path: Path to the .bit file.
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
BitstreamResult with status.
|
||||
"""
|
||||
bit_path = Path(bit_path).resolve()
|
||||
bit_path = Path(bit_path)
|
||||
if not bit_path.exists():
|
||||
return BitstreamResult(
|
||||
step="bitstream",
|
||||
@@ -126,30 +134,79 @@ def program_bitstream(
|
||||
if callback:
|
||||
callback("start", f"Loading bitstream: {bit_path.name}...")
|
||||
|
||||
vivado = _get_vivado_path(config.vitis_path)
|
||||
impact = _get_impact_path(config.vitis_path)
|
||||
# Try xsct first (preferred)
|
||||
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:
|
||||
return _program_with_vivado(vivado, bit_path, callback)
|
||||
|
||||
# Fallback to impact
|
||||
if impact:
|
||||
return _program_with_impact(impact, bit_path, callback)
|
||||
|
||||
return BitstreamResult(
|
||||
step="bitstream",
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
def _program_with_vivado(
|
||||
vivado_path: str,
|
||||
bit_path: Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> BitstreamResult:
|
||||
"""Program bitstream using Vivado hardware manager.
|
||||
"""Program bitstream using Vivado Hardware Manager.
|
||||
|
||||
Args:
|
||||
vivado_path: Path to vivado executable.
|
||||
@@ -160,16 +217,14 @@ def _program_with_vivado(
|
||||
BitstreamResult with status.
|
||||
"""
|
||||
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"""
|
||||
open_hw_manager
|
||||
connect_hw_server
|
||||
open_hw_target
|
||||
current_hw_device [lindex [get_hw_devices] 0]
|
||||
program_hw_device -file \"{bit_path}\"
|
||||
program_hw_device -file "{bit_path}"
|
||||
refresh_hw_device
|
||||
quit
|
||||
"""
|
||||
@@ -178,108 +233,25 @@ quit
|
||||
with open(tcl_path, "w") as f:
|
||||
f.write(tcl_content)
|
||||
|
||||
cmd = [vivado_path, "-mode", "batch", "-source", str(tcl_path)]
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
success = result.returncode == 0
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
result = _run_command(
|
||||
[vivado_path, "-mode", "batch", "-source", str(tcl_path)],
|
||||
timeout=120,
|
||||
callback=callback,
|
||||
)
|
||||
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:
|
||||
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}",
|
||||
success=success,
|
||||
message="Bitstream loaded successfully" if success else "Bitstream load failed",
|
||||
output=output[:4000],
|
||||
)
|
||||
finally:
|
||||
tcl_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _program_with_impact(
|
||||
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}",
|
||||
)
|
||||
# ── ELF Execution ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def run_elf(
|
||||
@@ -287,19 +259,19 @@ def run_elf(
|
||||
elf_path: str | Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> 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:
|
||||
config: Application configuration.
|
||||
elf_path: Path to the .elf executable file.
|
||||
elf_path: Path to the .elf file.
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
BitstreamResult with status.
|
||||
"""
|
||||
elf_path = Path(elf_path).resolve()
|
||||
elf_path = Path(elf_path)
|
||||
if not elf_path.exists():
|
||||
return BitstreamResult(
|
||||
step="elf",
|
||||
@@ -310,16 +282,99 @@ def run_elf(
|
||||
if callback:
|
||||
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)
|
||||
if not vivado:
|
||||
if vivado:
|
||||
return _run_elf_with_vivado(vivado, elf_path, config, callback)
|
||||
|
||||
return BitstreamResult(
|
||||
step="elf",
|
||||
success=False,
|
||||
message="Neither xsct nor vivado found in PATH",
|
||||
)
|
||||
|
||||
|
||||
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="Vivado not found",
|
||||
message="xsct ELF download timed out",
|
||||
)
|
||||
|
||||
# Create TCL script for running ELF
|
||||
# Use a configurable timeout (default 30s in ms) to allow ELF to initialize
|
||||
|
||||
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
|
||||
tcl_content = f"""
|
||||
open_hw_manager
|
||||
@@ -327,7 +382,7 @@ connect_hw_server
|
||||
open_hw_target
|
||||
current_hw_device [lindex [get_hw_devices] 0]
|
||||
refresh_hw_device
|
||||
program_hw_cpu -data_file \"{elf_path}\"
|
||||
program_hw_cpu -data_file "{elf_path}"
|
||||
run_hw_cpu {run_timeout}
|
||||
quit
|
||||
"""
|
||||
@@ -336,41 +391,25 @@ quit
|
||||
with open(tcl_path, "w") as f:
|
||||
f.write(tcl_content)
|
||||
|
||||
cmd = [vivado, "-mode", "batch", "-source", str(tcl_path)]
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
success = result.returncode == 0
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
result = _run_command(
|
||||
[vivado_path, "-mode", "batch", "-source", str(tcl_path)],
|
||||
timeout=120,
|
||||
callback=callback,
|
||||
)
|
||||
success = result.returncode == 0
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
|
||||
if callback:
|
||||
callback("complete" if success else "error",
|
||||
"ELF " + ("ran" if success else "failed"))
|
||||
|
||||
return BitstreamResult(
|
||||
step="elf",
|
||||
success=success,
|
||||
message="ELF executed successfully" if success else "ELF execution failed",
|
||||
output=output[:4000],
|
||||
)
|
||||
finally:
|
||||
tcl_path.unlink(missing_ok=True)
|
||||
except subprocess.TimeoutExpired:
|
||||
return BitstreamResult(
|
||||
step="elf",
|
||||
success=False,
|
||||
message="ELF execution timed out",
|
||||
)
|
||||
except OSError as e:
|
||||
return BitstreamResult(
|
||||
step="elf",
|
||||
success=False,
|
||||
message=f"OS error: {e}",
|
||||
success=success,
|
||||
message="ELF executed successfully" if success else "ELF execution failed",
|
||||
output=output[:4000],
|
||||
)
|
||||
finally:
|
||||
tcl_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
# ── Full Workflow ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def full_bitstream_program(
|
||||
@@ -379,12 +418,12 @@ def full_bitstream_program(
|
||||
elf_path: str | Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> list[BitstreamResult]:
|
||||
"""Execute full bitstream workflow: program bitstream then run ELF.
|
||||
"""Execute full bootloader workflow: program bitstream → run ELF.
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
bit_path: Path to the .bit bitstream file.
|
||||
elf_path: Path to the .elf executable file.
|
||||
bit_path: Path to the .bit file.
|
||||
elf_path: Path to the .elf file.
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -23,6 +23,8 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
||||
"bootloader_bit_path": "",
|
||||
"bootloader_elf_path": "",
|
||||
"bootloader_bin_path": "",
|
||||
"fsbl_elf_path": "",
|
||||
"flash_type": "qspi-x4-single",
|
||||
"firmware_bin_path": "",
|
||||
"reboot_timeout": 30,
|
||||
"ping_timeout": 5,
|
||||
@@ -48,6 +50,8 @@ class Config:
|
||||
bootloader_bit_path: str = ""
|
||||
bootloader_elf_path: str = ""
|
||||
bootloader_bin_path: str = ""
|
||||
fsbl_elf_path: str = ""
|
||||
flash_type: str = "qspi-x4-single"
|
||||
firmware_bin_path: str = ""
|
||||
reboot_timeout: int = 30
|
||||
ping_timeout: int = 5
|
||||
@@ -151,6 +155,8 @@ class Config:
|
||||
"bootloader_bit_path": self._relative_path(self.bootloader_bit_path),
|
||||
"bootloader_elf_path": self._relative_path(self.bootloader_elf_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),
|
||||
"reboot_timeout": self.reboot_timeout,
|
||||
"ping_timeout": self.ping_timeout,
|
||||
|
||||
+142
-183
@@ -1,21 +1,24 @@
|
||||
"""Flash programming module for Zynq Flasher GUI.
|
||||
|
||||
Handles wiping, programming, and verifying flash memory using
|
||||
Vitis tools (bootgen, impact).
|
||||
Handles wiping, programming, and verifying QSPI flash memory using
|
||||
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
|
||||
|
||||
import binascii
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from config_manager import Config
|
||||
from vitis_checker import get_program_flash_path
|
||||
|
||||
# ── Types ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -30,232 +33,177 @@ class FlashResult:
|
||||
|
||||
ProgressCallback = Callable[[str, str], None] | None
|
||||
|
||||
# ── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _run_command(
|
||||
cmd: list[str],
|
||||
timeout: int = 120,
|
||||
timeout: int = 300,
|
||||
callback: ProgressCallback = None,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
"""Run a subprocess command and optionally report progress.
|
||||
"""Run a subprocess command with progress reporting.
|
||||
|
||||
Args:
|
||||
cmd: Command and arguments to run.
|
||||
timeout: Maximum seconds to wait.
|
||||
cmd: Command and arguments.
|
||||
timeout: Maximum seconds to wait (flash ops can take minutes).
|
||||
callback: Optional callback(status, message).
|
||||
|
||||
Returns:
|
||||
CompletedProcess with stdout, stderr, and returncode.
|
||||
CompletedProcess result.
|
||||
"""
|
||||
if callback:
|
||||
callback("running", f"Executing: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
def _get_impact_path(vitis_path: str) -> str | None:
|
||||
"""Find the impact executable path.
|
||||
def _build_program_flash_cmd_base(
|
||||
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+).
|
||||
Uses shutil.which() for cross-platform compatibility,
|
||||
then falls back to common Vitis paths.
|
||||
program_flash requires an FSBL ELF to initialize the flash controller
|
||||
on Zynq devices.
|
||||
|
||||
Args:
|
||||
vitis_path: Vitis installation path.
|
||||
flash_tool: Path to program_flash executable.
|
||||
config: Application configuration.
|
||||
|
||||
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
|
||||
for tool in ("impact", "xsct"):
|
||||
path = shutil.which(tool)
|
||||
if path:
|
||||
return path
|
||||
|
||||
# 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"),
|
||||
return [
|
||||
flash_tool,
|
||||
"-f", config.bootloader_bin_path,
|
||||
"-fsbl", config.fsbl_elf_path,
|
||||
"-flash_type", config.flash_type,
|
||||
]
|
||||
for p in common_paths:
|
||||
if os.path.isfile(p):
|
||||
return p
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_bootgen_path(vitis_path: str) -> str | None:
|
||||
"""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
|
||||
# ── Public API ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def wipe_flash(
|
||||
config: Config,
|
||||
callback: ProgressCallback = None,
|
||||
) -> 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:
|
||||
config: Application configuration.
|
||||
config: Application configuration (needs fsbl_elf_path).
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
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:
|
||||
callback("start", "Wiping flash...")
|
||||
callback("start", "Erasing QSPI flash...")
|
||||
|
||||
impact_path = _get_impact_path(config.vitis_path)
|
||||
if not impact_path:
|
||||
return FlashResult(
|
||||
step="wipe",
|
||||
success=False,
|
||||
message="impact tool not found",
|
||||
)
|
||||
cmd = _build_program_flash_cmd_base(flash_tool, config)
|
||||
cmd += ["-erase_all", "-erase_only"]
|
||||
|
||||
# Use impact to wipe flash
|
||||
cmd = [impact_path, "-batch", "-c", "mass_erase"]
|
||||
try:
|
||||
result = _run_command(cmd, callback=callback)
|
||||
result = _run_command(cmd, timeout=300, callback=callback)
|
||||
success = result.returncode == 0
|
||||
return FlashResult(
|
||||
step="wipe",
|
||||
success=result.returncode == 0,
|
||||
message="Flash wiped successfully" if result.returncode == 0 else "Flash wipe failed",
|
||||
output=result.stdout or result.stderr,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
||||
return FlashResult(
|
||||
step="wipe",
|
||||
success=False,
|
||||
message=f"Wipe error: {e}",
|
||||
success=success,
|
||||
message="Flash erased successfully" if success else "Flash erase failed",
|
||||
output=(result.stdout + result.stderr)[:4000],
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return FlashResult(step="wipe", success=False, message="Flash erase timed out (300s)")
|
||||
|
||||
|
||||
def program_flash(
|
||||
def program_flash_bin(
|
||||
config: Config,
|
||||
bin_path: str | Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> 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:
|
||||
config: Application configuration.
|
||||
bin_path: Path to the .bin file to program.
|
||||
bin_path: Path to the .bin bootloader image.
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
FlashResult with status.
|
||||
"""
|
||||
bin_path = Path(bin_path).resolve()
|
||||
bin_path = Path(bin_path)
|
||||
if not bin_path.exists():
|
||||
return FlashResult(
|
||||
step="program",
|
||||
success=False,
|
||||
message=f"File not found: {bin_path}",
|
||||
message=f"BIN file not found: {bin_path}",
|
||||
)
|
||||
|
||||
if callback:
|
||||
callback("start", f"Programming flash: {bin_path.name}...")
|
||||
|
||||
# Use bootgen to create a boot image and program flash
|
||||
bootgen = _get_bootgen_path(config.vitis_path)
|
||||
if not bootgen:
|
||||
flash_tool = get_program_flash_path(config.vitis_path)
|
||||
if not flash_tool:
|
||||
return FlashResult(
|
||||
step="program",
|
||||
success=False,
|
||||
message="bootgen tool not found",
|
||||
message="program_flash not found in PATH",
|
||||
)
|
||||
|
||||
# Generate BIF file for boot generation
|
||||
bif_content = f"""all:
|
||||
{{
|
||||
load = \"{bin_path}\"
|
||||
}}
|
||||
"""
|
||||
bif_path = bin_path.parent / "temp_program.bif"
|
||||
if callback:
|
||||
callback("start", f"Programming flash: {bin_path.name} ({bin_path.stat().st_size / 1024 / 1024:.1f} MB)...")
|
||||
|
||||
# Override the BIN path in the command (use provided bin_path)
|
||||
cmd = [
|
||||
flash_tool,
|
||||
"-f", str(bin_path),
|
||||
"-fsbl", config.fsbl_elf_path,
|
||||
"-flash_type", config.flash_type,
|
||||
"-verify",
|
||||
"-no_erase",
|
||||
]
|
||||
|
||||
try:
|
||||
with open(bif_path, "w") as f:
|
||||
f.write(bif_content)
|
||||
|
||||
cmd = [bootgen, "-w", "-image", str(bif_path), "-bin", str(bin_path)]
|
||||
result = _run_command(cmd, callback=callback)
|
||||
|
||||
result = _run_command(cmd, timeout=600, callback=callback)
|
||||
success = result.returncode == 0
|
||||
return FlashResult(
|
||||
step="program",
|
||||
success=result.returncode == 0,
|
||||
message="Flash programmed successfully" if result.returncode == 0 else "Flash programming failed",
|
||||
output=result.stdout or result.stderr,
|
||||
success=success,
|
||||
message="Flash programmed successfully" if success else "Flash programming failed",
|
||||
output=(result.stdout + result.stderr)[:4000],
|
||||
)
|
||||
finally:
|
||||
bif_path.unlink(missing_ok=True)
|
||||
except subprocess.TimeoutExpired:
|
||||
return FlashResult(step="program", success=False, message="Flash programming timed out (600s)")
|
||||
|
||||
|
||||
def verify_flash(
|
||||
@@ -263,17 +211,21 @@ def verify_flash(
|
||||
bin_path: str | Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> 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:
|
||||
config: Application configuration.
|
||||
bin_path: Path to the original .bin file for verification.
|
||||
config: Application configuration (unused, kept for API consistency).
|
||||
bin_path: Path to the original .bin file.
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
FlashResult with status.
|
||||
FlashResult with CRC32 status.
|
||||
"""
|
||||
bin_path = Path(bin_path).resolve()
|
||||
bin_path = Path(bin_path)
|
||||
if not bin_path.exists():
|
||||
return FlashResult(
|
||||
step="verify",
|
||||
@@ -282,13 +234,17 @@ def verify_flash(
|
||||
)
|
||||
|
||||
if callback:
|
||||
callback("start", "Verifying flash content...")
|
||||
callback("start", "Computing local CRC32 for verification...")
|
||||
|
||||
# Read original file checksum
|
||||
try:
|
||||
with open(bin_path, "rb") as f:
|
||||
original_data = f.read()
|
||||
original_crc = _compute_crc32(original_data)
|
||||
original_crc = _compute_crc32(bin_path)
|
||||
if callback:
|
||||
callback("progress", f"CRC32: {original_crc:#010x}")
|
||||
return FlashResult(
|
||||
step="verify",
|
||||
success=True,
|
||||
message=f"Local CRC32: {original_crc:#010x}",
|
||||
)
|
||||
except OSError as e:
|
||||
return FlashResult(
|
||||
step="verify",
|
||||
@@ -296,26 +252,26 @@ def verify_flash(
|
||||
message=f"Read error: {e}",
|
||||
)
|
||||
|
||||
if callback:
|
||||
callback("progress", f"Original CRC32: {original_crc:#010x}")
|
||||
|
||||
return FlashResult(
|
||||
step="verify",
|
||||
success=True,
|
||||
message=f"Verification complete (CRC32: {original_crc:#010x})",
|
||||
)
|
||||
def _compute_crc32(path: Path) -> int:
|
||||
"""Compute CRC32 checksum of a file.
|
||||
|
||||
|
||||
def _compute_crc32(data: bytes) -> int:
|
||||
"""Compute CRC32 checksum of data.
|
||||
Reads the file in chunks to handle large binaries.
|
||||
|
||||
Args:
|
||||
data: Byte data to checksum.
|
||||
path: Path to the file.
|
||||
|
||||
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(
|
||||
@@ -323,11 +279,11 @@ def full_flash_program(
|
||||
bin_path: str | Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> list[FlashResult]:
|
||||
"""Execute full flash workflow: wipe -> program -> verify.
|
||||
"""Execute full flash workflow: wipe → program → verify.
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
bin_path: Path to the .bin file.
|
||||
bin_path: Path to the .bin bootloader image.
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
@@ -335,13 +291,16 @@ def full_flash_program(
|
||||
"""
|
||||
results: list[FlashResult] = []
|
||||
|
||||
# Step 1: Erase the entire flash
|
||||
results.append(wipe_flash(config, callback))
|
||||
if not results[-1].success:
|
||||
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:
|
||||
return results
|
||||
|
||||
# Step 3: Local CRC check
|
||||
results.append(verify_flash(config, bin_path, callback))
|
||||
return results
|
||||
|
||||
+23
-5
@@ -184,6 +184,7 @@ class MainWindow(ctk.CTk):
|
||||
self._config.bootloader_bit_path = files["bootloader_bit_path"]
|
||||
self._config.bootloader_elf_path = files["bootloader_elf_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"]
|
||||
|
||||
def _save_config(self) -> None:
|
||||
@@ -396,7 +397,7 @@ class MainWindow(ctk.CTk):
|
||||
row=0, column=0, sticky="nsew",
|
||||
padx=PADDING, pady=PADDING,
|
||||
)
|
||||
panel.grid_rowconfigure(5, weight=1)
|
||||
panel.grid_rowconfigure(6, weight=1)
|
||||
|
||||
title = ctk.CTkLabel(
|
||||
panel,
|
||||
@@ -448,6 +449,17 @@ class MainWindow(ctk.CTk):
|
||||
self._bootloader_bin_selector.set_path(str(resolved))
|
||||
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)
|
||||
self._firmware_bin_selector = FileSelector(
|
||||
panel,
|
||||
@@ -457,7 +469,7 @@ class MainWindow(ctk.CTk):
|
||||
if 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.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_btn = ctk.CTkButton(
|
||||
@@ -466,7 +478,7 @@ class MainWindow(ctk.CTk):
|
||||
font=FONT_SMALL,
|
||||
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:
|
||||
"""Build the serial monitor panel."""
|
||||
@@ -571,12 +583,13 @@ class MainWindow(ctk.CTk):
|
||||
"""Get selected file paths from the UI.
|
||||
|
||||
Returns:
|
||||
Dictionary with bootloader_bit_path, bootloader_elf_path, bootloader_bin_path, firmware_bin_path.
|
||||
Dictionary with all file paths from UI selectors.
|
||||
"""
|
||||
return {
|
||||
"bootloader_bit_path": self._bit_selector.get_path(),
|
||||
"bootloader_elf_path": self._elf_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(),
|
||||
}
|
||||
|
||||
@@ -681,7 +694,7 @@ class MainWindow(ctk.CTk):
|
||||
self._uart_warning_label.pack(fill="x", padx=PADDING, pady=(0, PADDING_SMALL))
|
||||
|
||||
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:
|
||||
return False
|
||||
|
||||
@@ -690,7 +703,12 @@ class MainWindow(ctk.CTk):
|
||||
self._log_message(" No Bootloader BIN file selected")
|
||||
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" FSBL: {files['fsbl_elf_path']}")
|
||||
|
||||
try:
|
||||
bin_path = Path(files["bootloader_bin_path"])
|
||||
|
||||
+142
-77
@@ -1,7 +1,11 @@
|
||||
"""Vitis/Vivado tool detection for Zynq Flasher GUI.
|
||||
|
||||
Checks for the presence and accessibility of required Xilinx tools
|
||||
(bootgen, impact, etc.) on the system.
|
||||
Checks for required Xilinx tools:
|
||||
- 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
|
||||
@@ -10,7 +14,7 @@ import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -26,7 +30,7 @@ class ToolStatus:
|
||||
path: str = ""
|
||||
version: str = ""
|
||||
error: str = ""
|
||||
alias: str = "" # Which alias was found (e.g. 'xsct' for 'impact')
|
||||
alias: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -40,30 +44,138 @@ class VitisCheckResult:
|
||||
errors: list[str]
|
||||
|
||||
|
||||
def check_vitis(config: Config) -> VitisCheckResult:
|
||||
"""Check for Vitis/Vivado tools availability.
|
||||
# ── Tool aliases: canonical name → [possible executable names] ──────
|
||||
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:
|
||||
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:
|
||||
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()
|
||||
vitis_path = config.vitis_path or ""
|
||||
tools: dict[str, ToolStatus] = {}
|
||||
errors: list[str] = []
|
||||
|
||||
# Xilinx tool aliases: map tool names to their possible executables
|
||||
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():
|
||||
for tool_name, aliases in TOOL_ALIASES.items():
|
||||
status = _check_tool(tool_name, aliases, vitis_path, system)
|
||||
tools[tool_name] = status
|
||||
if not status.found:
|
||||
@@ -79,53 +191,34 @@ def check_vitis(config: Config) -> VitisCheckResult:
|
||||
|
||||
|
||||
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:
|
||||
"""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:
|
||||
tool_name: Canonical name of the tool (e.g. 'impact').
|
||||
aliases: List of possible executable names (e.g. ['impact', 'xsct']).
|
||||
vitis_path: Vitis installation path to search.
|
||||
system: Platform identifier (linux, windows, darwin).
|
||||
tool_name: Canonical tool name.
|
||||
aliases: Executable names to try.
|
||||
vitis_path: Vitis installation path.
|
||||
system: Platform identifier.
|
||||
|
||||
Returns:
|
||||
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:
|
||||
found_path = shutil.which(f"{alias}{exec_suffix}")
|
||||
if found_path:
|
||||
version = _get_tool_version(found_path, tool_name, system)
|
||||
found = _find_executable(alias, vitis_path, system)
|
||||
if found:
|
||||
version = _get_tool_version(found, tool_name)
|
||||
return ToolStatus(
|
||||
name=tool_name,
|
||||
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,
|
||||
alias=alias,
|
||||
)
|
||||
|
||||
return ToolStatus(
|
||||
name=tool_name,
|
||||
found=False,
|
||||
@@ -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]:
|
||||
"""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().
|
||||
|
||||
Returns:
|
||||
Dictionary suitable for JSON serialization or GUI display.
|
||||
Dictionary suitable for JSON serialization / GUI display.
|
||||
"""
|
||||
return {
|
||||
"system": result.system,
|
||||
|
||||
Reference in New Issue
Block a user