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
+396
View File
@@ -0,0 +1,396 @@
"""Bitstream programming module for Zynq Flasher GUI.
Handles programming the Zynq device with a .bit bitstream file
and running an .elf executable afterward.
"""
from __future__ import annotations
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 BitstreamResult:
"""Result of a bitstream programming operation."""
step: str
success: bool
message: str
output: str = ""
ProgressCallback = Callable[[str, str], None] | None
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.
"""
path = shutil.which("impact")
if path:
return path
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_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
def _get_vivado_path(vitis_path: str) -> str | None:
"""Find the Vivado 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 Vivado executable, or None if not found.
"""
path = shutil.which("vivado")
if path:
return path
if vitis_path:
candidate = Path(vitis_path) / "bin" / "vivado"
if candidate.exists():
return str(candidate)
candidate = Path(vitis_path) / "bin" / "vivado.exe"
if candidate.exists():
return str(candidate)
common_paths = [
"/opt/xilinx/bin/vivado",
os.path.expanduser("~/Xilinx/Vivado/bin/vivado"),
]
for p in common_paths:
if os.path.isfile(p):
return p
return None
def program_bitstream(
config: Config,
bit_path: str | Path,
callback: ProgressCallback = None,
) -> BitstreamResult:
"""Program the Zynq device with a .bit bitstream file.
Uses Vivado/impact to download the bitstream to the FPGA fabric.
Args:
config: Application configuration.
bit_path: Path to the .bit bitstream file.
callback: Optional progress callback.
Returns:
BitstreamResult with status.
"""
bit_path = Path(bit_path).resolve()
if not bit_path.exists():
return BitstreamResult(
step="bitstream",
success=False,
message=f"Bitstream file not found: {bit_path}",
)
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 Vivado first (preferred for Zynq)
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",
)
def _program_with_vivado(
vivado_path: str,
bit_path: Path,
callback: ProgressCallback = None,
) -> BitstreamResult:
"""Program bitstream using Vivado hardware manager.
Args:
vivado_path: Path to vivado executable.
bit_path: Path to .bit file.
callback: Optional progress callback.
Returns:
BitstreamResult with status.
"""
if callback:
callback("progress", "Using Vivado to program bitstream...")
# 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}\"
refresh_hw_device
quit
"""
tcl_path = bit_path.parent / "temp_program.tcl"
try:
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()
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}",
)
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}",
)
def run_elf(
config: Config,
elf_path: str | Path,
callback: ProgressCallback = None,
) -> BitstreamResult:
"""Run an .elf executable on the Zynq device.
Uses Vivado hardware manager to download and run the ELF.
Args:
config: Application configuration.
elf_path: Path to the .elf executable file.
callback: Optional progress callback.
Returns:
BitstreamResult with status.
"""
elf_path = Path(elf_path).resolve()
if not elf_path.exists():
return BitstreamResult(
step="elf",
success=False,
message=f"ELF file not found: {elf_path}",
)
if callback:
callback("start", f"Running ELF: {elf_path.name}...")
vivado = _get_vivado_path(config.vitis_path)
if not vivado:
return BitstreamResult(
step="elf",
success=False,
message="Vivado not found",
)
# Create TCL script for running ELF
# Use a configurable timeout (default 30s in ms) to allow ELF to initialize
run_timeout = config.reboot_timeout * 1000 if config.reboot_timeout else 30000
tcl_content = f"""
open_hw_manager
connect_hw_server
open_hw_target
current_hw_device [lindex [get_hw_devices] 0]
refresh_hw_device
program_hw_cpu -data_file \"{elf_path}\"
run_hw_cpu {run_timeout}
quit
"""
tcl_path = elf_path.parent / "temp_run.tcl"
try:
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()
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}",
)
def full_bitstream_program(
config: Config,
bit_path: str | Path,
elf_path: str | Path,
callback: ProgressCallback = None,
) -> list[BitstreamResult]:
"""Execute full bitstream workflow: program bitstream then run ELF.
Args:
config: Application configuration.
bit_path: Path to the .bit bitstream file.
elf_path: Path to the .elf executable file.
callback: Optional progress callback.
Returns:
List of BitstreamResult for each step.
"""
results: list[BitstreamResult] = []
results.append(program_bitstream(config, bit_path, callback))
if not results[-1].success:
return results
results.append(run_elf(config, elf_path, callback))
return results