diff --git a/config/default_config.yaml b/config/default_config.yaml index c9f4307..b61477e 100644 --- a/config/default_config.yaml +++ b/config/default_config.yaml @@ -19,7 +19,12 @@ bootloader_bit_path: "" bootloader_elf_path: "" # Flash programming (Step 2: Program & Verify Flash) +# bootloader_bin_path is the bootloader image to program into QSPI flash +# fsbl_elf_path is the FSBL (First Stage Boot Loader) required by program_flash +# flash_type sets the QSPI interface mode (qspi-x4-single, qspi-x8-dual_parallel, etc.) bootloader_bin_path: "" +fsbl_elf_path: "" +flash_type: "qspi-x4-single" # Firmware upload (Step 4: Load Main Program via TFTP) firmware_bin_path: "" diff --git a/src/bitstream_programmer.py b/src/bitstream_programmer.py index 02b51e0..7390c05 100644 --- a/src/bitstream_programmer.py +++ b/src/bitstream_programmer.py @@ -1,13 +1,16 @@ """Bitstream programming module for Zynq Flasher GUI. -Handles programming the Zynq device with a .bit bitstream file -and running an .elf executable afterward. +Downloads a .bit bitstream to the Zynq PL fabric and runs a .elf +executable on the PS ARM core. + +Uses xsct (Xilinx Software Command Line Tool) as the primary tool +for JTAG operations, with Vivado batch mode as a fallback. +Compatible with Xilinx 2018.3 — 2023.2. """ from __future__ import annotations import os -import platform import shutil import subprocess from dataclasses import dataclass @@ -15,11 +18,14 @@ from pathlib import Path from typing import Callable from config_manager import Config +from vitis_checker import get_xsct_path + +# ── Types ─────────────────────────────────────────────────────────── @dataclass class BitstreamResult: - """Result of a bitstream programming operation.""" + """Result of a bitstream programming or ELF execution operation.""" step: str success: bool @@ -29,93 +35,95 @@ class BitstreamResult: ProgressCallback = Callable[[str, str], None] | None - -def _get_impact_path(vitis_path: str) -> str | None: - """Find the impact executable path. - - 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. - - Args: - vitis_path: Vitis installation path. - - Returns: - Path to impact or xsct executable, or None if not found. - """ - # Try shutil.which for impact, then xsct as fallback - for tool in ("impact", "xsct"): - path = shutil.which(tool) - if path: - return path - 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_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 +# ── Helpers ───────────────────────────────────────────────────────── def _get_vivado_path(vitis_path: str) -> str | None: - """Find the Vivado executable path. + """Find the Vivado executable. - Uses shutil.which() for cross-platform compatibility, - then falls back to common Vitis paths. + Uses shutil.which() for PATH resolution, then common install paths. Args: - vitis_path: Vitis installation path. + vitis_path: Optional Vitis installation directory. Returns: - Path to Vivado executable, or None if not found. + Path to vivado executable, or None. """ - path = shutil.which("vivado") - if path: - return path + found = shutil.which("vivado") + if found: + return found if vitis_path: candidate = Path(vitis_path) / "bin" / "vivado" - if candidate.exists(): + if candidate.is_file(): return str(candidate) - candidate = Path(vitis_path) / "bin" / "vivado.exe" - if candidate.exists(): - return str(candidate) - common_paths = [ + common = [ + "/data/xilinx/Vivado/2023.2/bin/vivado", "/opt/xilinx/bin/vivado", - os.path.expanduser("~/Xilinx/Vivado/bin/vivado"), + os.path.expanduser("~/Xilinx/Vivado/2023.2/bin/vivado"), ] - for p in common_paths: + for p in common: if os.path.isfile(p): return p return None +def _run_command( + cmd: list[str], + timeout: int = 120, + callback: ProgressCallback = None, +) -> subprocess.CompletedProcess[str]: + """Run a subprocess command with progress reporting. + + Args: + cmd: Command and arguments. + timeout: Maximum seconds. + callback: Optional callback(status, message). + + Returns: + CompletedProcess result. + + Raises: + subprocess.TimeoutExpired: If command exceeds timeout. + """ + if callback: + callback("running", f"Executing: {' '.join(cmd)}") + + 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 + + +# ── Bitstream Download ────────────────────────────────────────────── + + def program_bitstream( config: Config, bit_path: str | Path, callback: ProgressCallback = None, ) -> BitstreamResult: - """Program the Zynq device with a .bit bitstream file. + """Download a .bit bitstream to the Zynq PL (FPGA fabric). - Uses Vivado/impact to download the bitstream to the FPGA fabric. + Attempts xsct first (fast, lightweight), then falls back to Vivado + batch mode. Args: config: Application configuration. - bit_path: Path to the .bit bitstream file. + bit_path: Path to the .bit file. callback: Optional progress callback. Returns: BitstreamResult with status. """ - bit_path = Path(bit_path).resolve() + bit_path = Path(bit_path) if not bit_path.exists(): return BitstreamResult( step="bitstream", @@ -126,30 +134,79 @@ def program_bitstream( 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 xsct first (preferred) + xsct = get_xsct_path(config.vitis_path) + if xsct: + return _program_with_xsct(xsct, bit_path, callback) - # Try Vivado first (preferred for Zynq) + # Fallback to Vivado + vivado = _get_vivado_path(config.vitis_path) 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", + message="Neither xsct nor vivado found in PATH", ) +def _program_with_xsct( + xsct_path: str, + bit_path: Path, + callback: ProgressCallback = None, +) -> BitstreamResult: + """Program bitstream using xsct's fpga command. + + xsct connects to hw_server and uses the `fpga` command to download + the bitstream to the PL fabric. + + Args: + xsct_path: Path to xsct executable. + bit_path: Path to .bit file. + callback: Optional progress callback. + + Returns: + BitstreamResult with status. + """ + if callback: + callback("progress", "Using xsct to download bitstream...") + + # xsct TCL script: connect to hw_server, target FPGA, download bitstream + tcl_script = f""" +connect +targets +fpga -file "{bit_path}" +quit +""" + try: + result = _run_command( + [xsct_path, "-eval", tcl_script], + timeout=120, + callback=callback, + ) + success = result.returncode == 0 + output = (result.stdout + result.stderr).strip() + return BitstreamResult( + step="bitstream", + success=success, + message="Bitstream loaded successfully" if success else "Bitstream load failed", + output=output[:4000], + ) + except subprocess.TimeoutExpired: + return BitstreamResult( + step="bitstream", + success=False, + message="xsct bitstream download timed out", + ) + + def _program_with_vivado( vivado_path: str, bit_path: Path, callback: ProgressCallback = None, ) -> BitstreamResult: - """Program bitstream using Vivado hardware manager. + """Program bitstream using Vivado Hardware Manager. Args: vivado_path: Path to vivado executable. @@ -160,16 +217,14 @@ def _program_with_vivado( BitstreamResult with status. """ if callback: - callback("progress", "Using Vivado to program bitstream...") + callback("progress", "Using Vivado Hardware Manager...") - # 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}\" +program_hw_device -file "{bit_path}" refresh_hw_device quit """ @@ -178,108 +233,25 @@ quit 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() + result = _run_command( + [vivado_path, "-mode", "batch", "-source", str(tcl_path)], + timeout=120, + callback=callback, + ) + 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}", + success=success, + message="Bitstream loaded successfully" if success else "Bitstream load failed", + output=output[:4000], ) + finally: + tcl_path.unlink(missing_ok=True) -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}", - ) +# ── ELF Execution ─────────────────────────────────────────────────── def run_elf( @@ -287,19 +259,19 @@ def run_elf( elf_path: str | Path, callback: ProgressCallback = None, ) -> BitstreamResult: - """Run an .elf executable on the Zynq device. + """Download and run a .elf executable on the Zynq PS ARM core. - Uses Vivado hardware manager to download and run the ELF. + Uses xsct's `dow` (download) command, with Vivado as fallback. Args: config: Application configuration. - elf_path: Path to the .elf executable file. + elf_path: Path to the .elf file. callback: Optional progress callback. Returns: BitstreamResult with status. """ - elf_path = Path(elf_path).resolve() + elf_path = Path(elf_path) if not elf_path.exists(): return BitstreamResult( step="elf", @@ -310,16 +282,99 @@ def run_elf( if callback: callback("start", f"Running ELF: {elf_path.name}...") + # Try xsct first (preferred) + xsct = get_xsct_path(config.vitis_path) + if xsct: + return _run_elf_with_xsct(xsct, elf_path, callback) + + # Fallback to Vivado vivado = _get_vivado_path(config.vitis_path) - if not vivado: + if vivado: + return _run_elf_with_vivado(vivado, elf_path, config, callback) + + return BitstreamResult( + step="elf", + success=False, + message="Neither xsct nor vivado found in PATH", + ) + + +def _run_elf_with_xsct( + xsct_path: str, + elf_path: Path, + callback: ProgressCallback = None, +) -> BitstreamResult: + """Download and run ELF using xsct's dow command. + + xsct connects to hw_server, targets the first ARM Cortex-A9 core, + downloads the ELF, and starts execution. + + Args: + xsct_path: Path to xsct executable. + elf_path: Path to .elf file. + callback: Optional progress callback. + + Returns: + BitstreamResult with status. + """ + if callback: + callback("progress", "Using xsct to download ELF...") + + # xsct TCL: connect, target ARM core #0, download ELF + tcl_script = f""" +connect +targets -set -filter {{name =~ "Cortex-A9 MPCore #0" || name =~ "ARM*#0"}} +dow "{elf_path}" +con +quit +""" + try: + result = _run_command( + [xsct_path, "-eval", tcl_script], + timeout=60, + callback=callback, + ) + success = result.returncode == 0 + output = (result.stdout + result.stderr).strip() + + # xsct may return non-zero even if download succeeded (common quirk) + if "Target disconnected" in output and "Success" not in output: + success = True + + return BitstreamResult( + step="elf", + success=success, + message="ELF executed successfully" if success else "ELF execution failed", + output=output[:4000], + ) + except subprocess.TimeoutExpired: return BitstreamResult( step="elf", success=False, - message="Vivado not found", + message="xsct ELF download timed out", ) - # Create TCL script for running ELF - # Use a configurable timeout (default 30s in ms) to allow ELF to initialize + +def _run_elf_with_vivado( + vivado_path: str, + elf_path: Path, + config: Config, + callback: ProgressCallback = None, +) -> BitstreamResult: + """Download and run ELF using Vivado Hardware Manager. + + Args: + vivado_path: Path to vivado executable. + elf_path: Path to .elf file. + config: Application configuration (for reboot_timeout). + callback: Optional progress callback. + + Returns: + BitstreamResult with status. + """ + if callback: + callback("progress", "Using Vivado Hardware Manager for ELF...") + run_timeout = config.reboot_timeout * 1000 if config.reboot_timeout else 30000 tcl_content = f""" open_hw_manager @@ -327,7 +382,7 @@ connect_hw_server open_hw_target current_hw_device [lindex [get_hw_devices] 0] refresh_hw_device -program_hw_cpu -data_file \"{elf_path}\" +program_hw_cpu -data_file "{elf_path}" run_hw_cpu {run_timeout} quit """ @@ -336,41 +391,25 @@ quit 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() + result = _run_command( + [vivado_path, "-mode", "batch", "-source", str(tcl_path)], + timeout=120, + callback=callback, + ) + 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}", + success=success, + message="ELF executed successfully" if success else "ELF execution failed", + output=output[:4000], ) + finally: + tcl_path.unlink(missing_ok=True) + + +# ── Full Workflow ─────────────────────────────────────────────────── def full_bitstream_program( @@ -379,12 +418,12 @@ def full_bitstream_program( elf_path: str | Path, callback: ProgressCallback = None, ) -> list[BitstreamResult]: - """Execute full bitstream workflow: program bitstream then run ELF. + """Execute full bootloader workflow: program bitstream → run ELF. Args: config: Application configuration. - bit_path: Path to the .bit bitstream file. - elf_path: Path to the .elf executable file. + bit_path: Path to the .bit file. + elf_path: Path to the .elf file. callback: Optional progress callback. Returns: diff --git a/src/config_manager.py b/src/config_manager.py index ce030ba..1103764 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -23,6 +23,8 @@ DEFAULT_CONFIG: dict[str, Any] = { "bootloader_bit_path": "", "bootloader_elf_path": "", "bootloader_bin_path": "", + "fsbl_elf_path": "", + "flash_type": "qspi-x4-single", "firmware_bin_path": "", "reboot_timeout": 30, "ping_timeout": 5, @@ -48,6 +50,8 @@ class Config: bootloader_bit_path: str = "" bootloader_elf_path: str = "" bootloader_bin_path: str = "" + fsbl_elf_path: str = "" + flash_type: str = "qspi-x4-single" firmware_bin_path: str = "" reboot_timeout: int = 30 ping_timeout: int = 5 @@ -151,6 +155,8 @@ class Config: "bootloader_bit_path": self._relative_path(self.bootloader_bit_path), "bootloader_elf_path": self._relative_path(self.bootloader_elf_path), "bootloader_bin_path": self._relative_path(self.bootloader_bin_path), + "fsbl_elf_path": self._relative_path(self.fsbl_elf_path), + "flash_type": self.flash_type, "firmware_bin_path": self._relative_path(self.firmware_bin_path), "reboot_timeout": self.reboot_timeout, "ping_timeout": self.ping_timeout, diff --git a/src/flash_programmer.py b/src/flash_programmer.py index 8584824..88b9e8e 100644 --- a/src/flash_programmer.py +++ b/src/flash_programmer.py @@ -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 diff --git a/src/gui/main_window.py b/src/gui/main_window.py index fa71de1..d4facd7 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -184,6 +184,7 @@ class MainWindow(ctk.CTk): self._config.bootloader_bit_path = files["bootloader_bit_path"] self._config.bootloader_elf_path = files["bootloader_elf_path"] self._config.bootloader_bin_path = files["bootloader_bin_path"] + self._config.fsbl_elf_path = files["fsbl_elf_path"] self._config.firmware_bin_path = files["firmware_bin_path"] def _save_config(self) -> None: @@ -396,7 +397,7 @@ class MainWindow(ctk.CTk): row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING, ) - panel.grid_rowconfigure(5, weight=1) + panel.grid_rowconfigure(6, weight=1) title = ctk.CTkLabel( panel, @@ -448,6 +449,17 @@ class MainWindow(ctk.CTk): self._bootloader_bin_selector.set_path(str(resolved)) self._bootloader_bin_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) + # FSBL ELF path (required by program_flash for QSPI programming) + self._fsbl_selector = FileSelector( + panel, + label_text="FSBL ELF (.elf):", + file_types=[("ELF files", "*.elf"), ("All files", "*")], + ) + if self._config.fsbl_elf_path: + resolved = self._config.resolve_path(self._config.fsbl_elf_path) + self._fsbl_selector.set_path(str(resolved)) + self._fsbl_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) + # Firmware BIN path (TFTP upload) self._firmware_bin_selector = FileSelector( panel, @@ -457,7 +469,7 @@ class MainWindow(ctk.CTk): if self._config.firmware_bin_path: resolved = self._config.resolve_path(self._config.firmware_bin_path) self._firmware_bin_selector.set_path(str(resolved)) - self._firmware_bin_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) + self._firmware_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) # Save button save_btn = ctk.CTkButton( @@ -466,7 +478,7 @@ class MainWindow(ctk.CTk): font=FONT_SMALL, command=self._save_config, ) - save_btn.grid(row=6, column=0, padx=PADDING, pady=PADDING) + save_btn.grid(row=7, column=0, padx=PADDING, pady=PADDING) def _build_serial_panel(self, parent: ctk.CTkFrame) -> None: """Build the serial monitor panel.""" @@ -571,12 +583,13 @@ class MainWindow(ctk.CTk): """Get selected file paths from the UI. Returns: - Dictionary with bootloader_bit_path, bootloader_elf_path, bootloader_bin_path, firmware_bin_path. + Dictionary with all file paths from UI selectors. """ return { "bootloader_bit_path": self._bit_selector.get_path(), "bootloader_elf_path": self._elf_selector.get_path(), "bootloader_bin_path": self._bootloader_bin_selector.get_path(), + "fsbl_elf_path": self._fsbl_selector.get_path(), "firmware_bin_path": self._firmware_bin_selector.get_path(), } @@ -681,7 +694,7 @@ class MainWindow(ctk.CTk): self._uart_warning_label.pack(fill="x", padx=PADDING, pady=(0, PADDING_SMALL)) def _run_step_2_program_flash(self) -> bool: - """Step 2: Program and verify Flash.""" + """Step 2: Program and verify Flash using program_flash.""" if not self._config: return False @@ -690,7 +703,12 @@ class MainWindow(ctk.CTk): self._log_message(" No Bootloader BIN file selected") return False + if not files["fsbl_elf_path"]: + self._log_message(" No FSBL ELF file selected (required by program_flash)") + return False + self._log_message(f"Step 2: Programming Flash with {files['bootloader_bin_path']}...") + self._log_message(f" FSBL: {files['fsbl_elf_path']}") try: bin_path = Path(files["bootloader_bin_path"]) diff --git a/src/vitis_checker.py b/src/vitis_checker.py index 5325e9a..b701da8 100644 --- a/src/vitis_checker.py +++ b/src/vitis_checker.py @@ -1,7 +1,11 @@ """Vitis/Vivado tool detection for Zynq Flasher GUI. -Checks for the presence and accessibility of required Xilinx tools -(bootgen, impact, etc.) on the system. +Checks for required Xilinx tools: + - xsct (Xilinx Software Command Line Tool) — JTAG operations + - program_flash — QSPI flash programming + - bootgen — BOOT.BIN generation (image creation only, not hardware) + +Provides shared helpers used by flash_programmer and bitstream_programmer. """ from __future__ import annotations @@ -10,7 +14,7 @@ import os import platform import shutil import subprocess -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from typing import Any @@ -26,7 +30,7 @@ class ToolStatus: path: str = "" version: str = "" error: str = "" - alias: str = "" # Which alias was found (e.g. 'xsct' for 'impact') + alias: str = "" @dataclass @@ -40,30 +44,138 @@ class VitisCheckResult: errors: list[str] -def check_vitis(config: Config) -> VitisCheckResult: - """Check for Vitis/Vivado tools availability. +# ── Tool aliases: canonical name → [possible executable names] ────── +TOOL_ALIASES: dict[str, list[str]] = { + "xsct": ["xsct"], + "program_flash": ["program_flash"], + "bootgen": ["bootgen"], +} - Scans the configured vitis_path and system PATH for required tools: - bootgen (boot image generator), impact (JTAG controller). + +def _find_executable(exec_name: str, vitis_path: str, system: str) -> str | None: + """Find an executable by name using PATH and Vitis search paths. Args: - config: Application configuration with vitis_path setting. + exec_name: Executable name (without extension). + vitis_path: Vitis/Vivado installation root directory. + system: Platform identifier (linux, windows, darwin). Returns: - VitisCheckResult with details on each tool's status. + Absolute path to the executable, or None. + """ + exec_suffix = ".exe" if system == "windows" else "" + + # 1. shutil.which() — most reliable cross-platform PATH resolution + found = shutil.which(f"{exec_name}{exec_suffix}") + if found: + return found + + # 2. Scan vitis_path/bin + if vitis_path: + candidate = Path(vitis_path) / "bin" / f"{exec_name}{exec_suffix}" + if candidate.is_file(): + return str(candidate) + + # 3. Common Xilinx install paths + common_paths = [ + f"/data/xilinx/Vitis/2023.2/bin/{exec_name}", + f"/data/xilinx/Vivado/2023.2/bin/{exec_name}", + f"/opt/xilinx/bin/{exec_name}", + os.path.expanduser(f"~/Xilinx/Vitis/2023.2/bin/{exec_name}"), + os.path.expanduser(f"~/Xilinx/Vivado/2023.2/bin/{exec_name}"), + ] + for p in common_paths: + if os.path.isfile(p): + return p + + return None + + +def get_xsct_path(vitis_path: str = "") -> str | None: + """Find the xsct (Xilinx Software Command Line Tool) executable. + + xsct is the primary JTAG scripting tool for Vivado/Vitis (2015.x+). + + Args: + vitis_path: Optional Vitis installation directory. + + Returns: + Absolute path to xsct, or None. + """ + return _find_executable("xsct", vitis_path, platform.system().lower()) + + +def get_program_flash_path(vitis_path: str = "") -> str | None: + """Find the program_flash executable (QSPI flash programming). + + program_flash is part of Vitis/SDK (2014.x+). + + Args: + vitis_path: Optional Vitis installation directory. + + Returns: + Absolute path to program_flash, or None. + """ + return _find_executable("program_flash", vitis_path, platform.system().lower()) + + +def get_bootgen_path(vitis_path: str = "") -> str | None: + """Find the bootgen executable (BOOT.BIN generation). + + bootgen creates boot images from BIF descriptions. It does NOT + program flash — use program_flash for that. + + Args: + vitis_path: Optional Vitis installation directory. + + Returns: + Absolute path to bootgen, or None. + """ + return _find_executable("bootgen", vitis_path, platform.system().lower()) + + +def _get_tool_version(tool_path: str, tool_name: str) -> str: + """Get the version/h banner from a tool executable. + + Args: + tool_path: Absolute path to the tool. + tool_name: Canonical tool name. + + Returns: + First line of help output (truncated to 120 chars), or empty string. + """ + try: + result = subprocess.run( + [tool_path, "-help"], + capture_output=True, + text=True, + timeout=10, + ) + output = (result.stdout + result.stderr).strip() + if output: + return output.split("\n")[0][:120] + except (subprocess.TimeoutExpired, OSError): + pass + return "" + + +def check_vitis(config: Config) -> VitisCheckResult: + """Check availability of all required Xilinx tools. + + Scans PATH and Vitis paths for: xsct, program_flash, bootgen. + + Args: + config: Application configuration. + + Returns: + VitisCheckResult with per-tool status. """ system = platform.system().lower() vitis_path = config.vitis_path or "" tools: dict[str, ToolStatus] = {} errors: list[str] = [] - # Xilinx tool aliases: map tool names to their possible executables - tool_aliases: dict[str, list[str]] = { - "bootgen": ["bootgen"], - "impact": ["impact", "xsct"], # xsct can replace impact in newer versions - } - - for tool_name, aliases in tool_aliases.items(): + for tool_name, aliases in TOOL_ALIASES.items(): status = _check_tool(tool_name, aliases, vitis_path, system) tools[tool_name] = status if not status.found: @@ -79,53 +191,34 @@ def check_vitis(config: Config) -> VitisCheckResult: def _check_tool( - tool_name: str, aliases: list[str], vitis_path: str, system: str + tool_name: str, + aliases: list[str], + vitis_path: str, + system: str, ) -> ToolStatus: """Check if a single tool is available. - Uses shutil.which() for reliable PATH resolution, then falls back - to scanning vitis_path/bin for each alias. - Args: - tool_name: Canonical name of the tool (e.g. 'impact'). - aliases: List of possible executable names (e.g. ['impact', 'xsct']). - vitis_path: Vitis installation path to search. - system: Platform identifier (linux, windows, darwin). + tool_name: Canonical tool name. + aliases: Executable names to try. + vitis_path: Vitis installation path. + system: Platform identifier. Returns: ToolStatus with detection results. """ - exec_suffix = ".exe" if system == "windows" else "" - - # 1. Try shutil.which() for each alias — most reliable PATH resolution for alias in aliases: - found_path = shutil.which(f"{alias}{exec_suffix}") - if found_path: - version = _get_tool_version(found_path, tool_name, system) + found = _find_executable(alias, vitis_path, system) + if found: + version = _get_tool_version(found, tool_name) return ToolStatus( name=tool_name, found=True, - path=found_path, + path=found, version=version, alias=alias, ) - # 2. Try vitis_path/bin for each alias - if vitis_path: - bin_dir = Path(vitis_path) / "bin" - if bin_dir.exists(): - for alias in aliases: - candidate = bin_dir / f"{alias}{exec_suffix}" - if candidate.exists() and candidate.is_file(): - version = _get_tool_version(str(candidate), tool_name, system) - return ToolStatus( - name=tool_name, - found=True, - path=str(candidate), - version=version, - alias=alias, - ) - return ToolStatus( name=tool_name, found=False, @@ -137,34 +230,6 @@ def _check_tool( ) -def _get_tool_version(tool_path: str, tool_name: str, system: str) -> str: - """Get version string from a tool executable. - - Args: - tool_path: Absolute path to the tool executable. - tool_name: Name of the tool. - system: Platform identifier. - - Returns: - Version string or empty string if unavailable. - """ - try: - flag = "-h" if tool_name == "impact" else "-h" - result = subprocess.run( - [tool_path, flag], - capture_output=True, - text=True, - timeout=10, - ) - output = (result.stdout + result.stderr).strip() - if output: - first_line = output.split("\n")[0] - return first_line[:120] - except (subprocess.TimeoutExpired, OSError, FileNotFoundError): - pass - return "" - - def get_tool_status_dict(result: VitisCheckResult) -> dict[str, Any]: """Convert a VitisCheckResult to a serializable dictionary. @@ -172,7 +237,7 @@ def get_tool_status_dict(result: VitisCheckResult) -> dict[str, Any]: result: VitisCheckResult from check_vitis(). Returns: - Dictionary suitable for JSON serialization or GUI display. + Dictionary suitable for JSON serialization / GUI display. """ return { "system": result.system,