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