feat: initial Zynq XC7Z100 Flasher GUI project
Add cross-platform GUI application for programming Zynq devices with step-by-step workflow: - CustomTkinter GUI with dark/light theme support - 4-step workflow: Check Env → Flash → Bootloader → Main Program - YAML configuration with relative path resolution - Serial monitoring and boot log parsing - TFTP upload with CRC32 verification - SSH/serial reboot management - Vivado/Impact tool auto-detection Project structure: - app.py: Entry point - src/: Core modules (config, flash, bitstream, tftp, serial, gui) - config/: Default configuration template - .flasher_env/: Python virtual environment
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
"""Flash programming module for Zynq Flasher GUI.
|
||||
|
||||
Handles wiping, programming, and verifying flash memory using
|
||||
Vitis tools (bootgen, impact).
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlashResult:
|
||||
"""Result of a flash programming operation."""
|
||||
|
||||
step: str
|
||||
success: bool
|
||||
message: str
|
||||
output: str = ""
|
||||
|
||||
|
||||
ProgressCallback = Callable[[str, str], None] | None
|
||||
|
||||
|
||||
def _run_command(
|
||||
cmd: list[str],
|
||||
timeout: int = 120,
|
||||
callback: ProgressCallback = None,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
"""Run a subprocess command and optionally report progress.
|
||||
|
||||
Args:
|
||||
cmd: Command and arguments to run.
|
||||
timeout: Maximum seconds to wait.
|
||||
callback: Optional callback(status, message).
|
||||
|
||||
Returns:
|
||||
CompletedProcess with stdout, stderr, and returncode.
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
def _get_impact_path(vitis_path: str) -> str | None:
|
||||
"""Find the impact 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 impact executable, or None if not found.
|
||||
"""
|
||||
# Try shutil.which first (cross-platform)
|
||||
path = shutil.which("impact")
|
||||
if path:
|
||||
return path
|
||||
|
||||
# Search in vitis_path/bin
|
||||
if vitis_path:
|
||||
candidate = Path(vitis_path) / "bin" / "impact"
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
candidate = Path(vitis_path) / "bin" / "impact.exe"
|
||||
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"),
|
||||
]
|
||||
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
|
||||
|
||||
|
||||
def wipe_flash(
|
||||
config: Config,
|
||||
callback: ProgressCallback = None,
|
||||
) -> FlashResult:
|
||||
"""Wipe the flash memory.
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
FlashResult with status.
|
||||
"""
|
||||
if callback:
|
||||
callback("start", "Wiping flash...")
|
||||
|
||||
impact_path = _get_impact_path(config.vitis_path)
|
||||
if not impact_path:
|
||||
return FlashResult(
|
||||
step="wipe",
|
||||
success=False,
|
||||
message="impact tool not found",
|
||||
)
|
||||
|
||||
# Use impact to wipe flash
|
||||
cmd = [impact_path, "-batch", "-c", "mass_erase"]
|
||||
try:
|
||||
result = _run_command(cmd, callback=callback)
|
||||
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}",
|
||||
)
|
||||
|
||||
|
||||
def program_flash(
|
||||
config: Config,
|
||||
bin_path: str | Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> FlashResult:
|
||||
"""Program the flash with a binary file.
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
bin_path: Path to the .bin file to program.
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
FlashResult with status.
|
||||
"""
|
||||
bin_path = Path(bin_path).resolve()
|
||||
if not bin_path.exists():
|
||||
return FlashResult(
|
||||
step="program",
|
||||
success=False,
|
||||
message=f"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:
|
||||
return FlashResult(
|
||||
step="program",
|
||||
success=False,
|
||||
message="bootgen tool not found",
|
||||
)
|
||||
|
||||
# Generate BIF file for boot generation
|
||||
bif_content = f"""all:
|
||||
{{
|
||||
load = \"{bin_path}\"
|
||||
}}
|
||||
"""
|
||||
bif_path = bin_path.parent / "temp_program.bif"
|
||||
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)
|
||||
|
||||
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,
|
||||
)
|
||||
finally:
|
||||
bif_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def verify_flash(
|
||||
config: Config,
|
||||
bin_path: str | Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> FlashResult:
|
||||
"""Verify the programmed flash content.
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
bin_path: Path to the original .bin file for verification.
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
FlashResult with status.
|
||||
"""
|
||||
bin_path = Path(bin_path).resolve()
|
||||
if not bin_path.exists():
|
||||
return FlashResult(
|
||||
step="verify",
|
||||
success=False,
|
||||
message=f"File not found: {bin_path}",
|
||||
)
|
||||
|
||||
if callback:
|
||||
callback("start", "Verifying flash content...")
|
||||
|
||||
# Read original file checksum
|
||||
try:
|
||||
with open(bin_path, "rb") as f:
|
||||
original_data = f.read()
|
||||
original_crc = _compute_crc32(original_data)
|
||||
except OSError as e:
|
||||
return FlashResult(
|
||||
step="verify",
|
||||
success=False,
|
||||
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(data: bytes) -> int:
|
||||
"""Compute CRC32 checksum of data.
|
||||
|
||||
Args:
|
||||
data: Byte data to checksum.
|
||||
|
||||
Returns:
|
||||
CRC32 value as unsigned integer.
|
||||
"""
|
||||
return binascii.crc32(data) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def full_flash_program(
|
||||
config: Config,
|
||||
bin_path: str | Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> list[FlashResult]:
|
||||
"""Execute full flash workflow: wipe -> program -> verify.
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
bin_path: Path to the .bin file.
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
List of FlashResult for each step.
|
||||
"""
|
||||
results: list[FlashResult] = []
|
||||
|
||||
results.append(wipe_flash(config, callback))
|
||||
if not results[-1].success:
|
||||
return results
|
||||
|
||||
results.append(program_flash(config, bin_path, callback))
|
||||
if not results[-1].success:
|
||||
return results
|
||||
|
||||
results.append(verify_flash(config, bin_path, callback))
|
||||
return results
|
||||
Reference in New Issue
Block a user