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