♻️ 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:
|
||||
|
||||
Reference in New Issue
Block a user