♻️ 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:
Jeremy Shen
2026-06-10 12:13:02 +08:00
parent 1ad2dcdeb9
commit cddacc5d9f
6 changed files with 567 additions and 475 deletions
+142 -77
View File
@@ -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,