♻️ 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
+249 -210
View File
@@ -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:
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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
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,