Files
Zynq_Flasher/src/flash_programmer.py
T
yuysh 17c8f2d68b refactor: replace all config.vitis_path with config.xilinx_path across codebase
Affected files:
- flash_programmer.py (2 call sites) — get_program_flash_path
- bitstream_programmer.py (3 get_xsct_path + 1 get_vivado_path call sites)
  Also: removed duplicate _get_vivado_path() (now uses shared one from vitis_checker)
  Also: removed unused shutil import
- reboot_manager.py (1 call site) — get_xsct_path

All now use getattr(config, 'xilinx_path', '') consistently.
The only remaining 'config.vitis_path' reference is in vitis_checker.py's
check_vitis() itself (safe legacy fallback for old configs).
2026-06-11 15:33:54 +08:00

368 lines
11 KiB
Python

"""Flash programming module for Zynq Flasher GUI.
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 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
class FlashResult:
"""Result of a flash programming operation."""
step: str
success: bool
message: str
output: str = ""
ProgressCallback = Callable[[str, str], None] | None
# ── Helpers ─────────────────────────────────────────────────────────
def _run_command(
cmd: list[str],
timeout: int = 300,
callback: ProgressCallback = None,
) -> subprocess.CompletedProcess[str]:
"""Run a subprocess command with real-time streaming output.
Uses subprocess_utils.run_streaming to stream stdout/stderr in
real time, parsing for warnings, errors, and progress.
Args:
cmd: Command and arguments.
timeout: Maximum seconds.
callback: Optional callback(status, message).
Returns:
CompletedProcess result.
"""
from subprocess_utils import run_streaming
if callback:
callback("running", f"Executing: {' '.join(cmd)}")
try:
return run_streaming(cmd, timeout=timeout, callback=callback)
except subprocess.TimeoutExpired:
if callback:
callback("error", f"{cmd[0]} timed out after {timeout}s")
raise
def _build_program_flash_cmd_base(
flash_tool: str,
config: Config,
) -> list[str]:
"""Build the base command for program_flash.
program_flash requires an FSBL ELF to initialize the flash controller
on Zynq devices.
Args:
flash_tool: Path to program_flash executable.
config: Application configuration.
Returns:
Base command list without the operation-specific flags.
"""
return [
flash_tool,
"-f", config.bootloader_bin_path,
"-fsbl", config.fsbl_elf_path,
"-flash_type", config.flash_type,
]
# ── Public API ──────────────────────────────────────────────────────
def wipe_flash(
config: Config,
callback: ProgressCallback = None,
) -> FlashResult:
"""Erase QSPI flash before programming.
- erase_all=False: -erase_only (matches BIN size, works on all chips)
- erase_all=True: creates a temp file of full flash capacity, uses
-erase_only with it. Avoids -erase_all flag which fails on
s25fl256s1 (no memory density info).
Args:
config: Application configuration (needs fsbl_elf_path).
callback: Optional progress callback.
Returns:
FlashResult with status.
"""
flash_tool = get_program_flash_path(
xilinx_root=getattr(config, "xilinx_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", "Erasing QSPI flash...")
erase_all = getattr(config, 'erase_all', False)
try:
if erase_all:
flash_size = _flash_capacity(config)
import tempfile, os
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.bin')
try:
tmp.seek(flash_size - 1)
tmp.write(b'\x00')
tmp.close()
cmd = [
flash_tool,
"-f", tmp.name,
"-fsbl", str(fsbl_path),
"-flash_type", config.flash_type,
"-erase_only",
]
result = _run_command(
cmd,
timeout=config.step_timeouts.get("flash_erase", 120),
callback=callback,
)
finally:
os.unlink(tmp.name)
else:
cmd = _build_program_flash_cmd_base(flash_tool, config)
cmd += ["-erase_only"]
result = _run_command(
cmd,
timeout=config.step_timeouts.get("flash_erase", 120),
callback=callback,
)
success = result.returncode == 0
return FlashResult(
step="wipe",
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")
except FileNotFoundError:
return FlashResult(step="wipe", success=False, message="program_flash not found")
def _flash_capacity(config: Config) -> int:
"""Get flash chip capacity in bytes from flash_model.
Args:
config: Application configuration.
Returns:
Capacity in bytes (default 32 MiB).
"""
model = getattr(config, 'flash_model', 's25fl256s1').lower()
capacities: dict[str, int] = {
's25fl256': 32 * 1024 * 1024,
's25fl128': 16 * 1024 * 1024,
's25fl064': 8 * 1024 * 1024,
}
for key, cap in capacities.items():
if key in model:
return cap
return 32 * 1024 * 1024
def program_flash_bin(
config: Config,
bin_path: str | Path,
callback: ProgressCallback = None,
) -> FlashResult:
"""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 bootloader image.
callback: Optional progress callback.
Returns:
FlashResult with status.
"""
bin_path = Path(bin_path)
if not bin_path.exists():
return FlashResult(
step="program",
success=False,
message=f"BIN file not found: {bin_path}",
)
flash_tool = get_program_flash_path(
xilinx_root=getattr(config, "xilinx_path", ""),
)
if not flash_tool:
return FlashResult(
step="program",
success=False,
message="program_flash not found in PATH",
)
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:
result = _run_command(cmd, timeout=config.step_timeouts.get("flash_program", 600), callback=callback)
success = result.returncode == 0
return FlashResult(
step="program",
success=success,
message="Flash programmed successfully" if success else "Flash programming failed",
output=(result.stdout + result.stderr)[:4000],
)
except subprocess.TimeoutExpired:
return FlashResult(step="program", success=False, message="Flash programming timed out (600s)")
def verify_flash(
config: Config,
bin_path: str | Path,
callback: ProgressCallback = None,
) -> FlashResult:
"""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 (unused, kept for API consistency).
bin_path: Path to the original .bin file.
callback: Optional progress callback.
Returns:
FlashResult with CRC32 status.
"""
bin_path = Path(bin_path)
if not bin_path.exists():
return FlashResult(
step="verify",
success=False,
message=f"File not found: {bin_path}",
)
if callback:
callback("start", "Computing local CRC32 for verification...")
try:
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",
success=False,
message=f"Read error: {e}",
)
def _compute_crc32(path: Path) -> int:
"""Compute CRC32 checksum of a file.
Reads the file in chunks to handle large binaries.
Args:
path: Path to the file.
Returns:
CRC32 value as unsigned 32-bit integer.
"""
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(
config: Config,
bin_path: str | Path,
callback: ProgressCallback = None,
) -> list[FlashResult]:
"""Execute full flash workflow: wipe → program → verify.
Erase scope is controlled by config.erase_all:
- True: entire flash chip (uses -erase_all)
- False: sectors matching BIN file size (uses -erase_only)
Args:
config: Application configuration.
bin_path: Path to the .bin bootloader image.
callback: Optional progress callback.
Returns:
List of FlashResult for each step.
"""
results: list[FlashResult] = []
# Step 1: Erase (scope determined by config.erase_all)
results.append(wipe_flash(config, callback))
if not results[-1].success:
return results
# 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