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,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
|
||||
Reference in New Issue
Block a user