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:
Jeremy Shen
2026-06-08 11:33:50 +08:00
commit bd39023ba7
19 changed files with 3859 additions and 0 deletions
+344
View File
@@ -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