♻️ 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: