#!/usr/bin/env python3 """Guard and program a Zynq BOOT.bin through Xilinx program_flash. Success requires erase, program, verify, and the final Xilinx success marker; the process exit code alone is deliberately not trusted. """ import argparse import hashlib import os import re import socket import subprocess import sys from pathlib import Path from vivado_env import resolve_sdk_dir def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest().upper() def parse_server(url: str): match = re.fullmatch(r"tcp:([^:]+):(\d+)", url) if not match: raise SystemExit("ERROR: --hw-server-url must look like tcp:127.0.0.1:3121") return match.group(1), int(match.group(2)) def main() -> None: parser = argparse.ArgumentParser(description="Program and verify Zynq QSPI BOOT.bin") parser.add_argument("--sdk-dir", help="Xilinx SDK root containing bin/program_flash.bat") parser.add_argument("--image", required=True, help="BOOT.bin to program at offset 0") parser.add_argument("--jtag-fsbl", required=True, help="helper FSBL loaded to OCM for programming") parser.add_argument("--expect-sha256", required=True, help="exact expected BOOT.bin SHA-256") parser.add_argument("--expect-size", type=int, help="exact expected BOOT.bin byte length") parser.add_argument("--capacity-bytes", type=int, default=32 * 1024 * 1024) parser.add_argument("--expect-jtag-fsbl-sha256", help="optional exact helper FSBL SHA-256") parser.add_argument( "--require-marker", action="append", default=[], help="ASCII marker required in BOOT.bin; repeatable", ) parser.add_argument( "--flash-type", choices=["qspi_single", "qspi_x1_single", "qspi_x2_single", "qspi_dual_parallel", "qspi_dual_stacked", "qspi_x1_dual_stacked", "qspi_x2_dual_stacked"], default="qspi_dual_parallel", ) parser.add_argument("--offset", default="0") parser.add_argument("--hw-server-url", default="tcp:127.0.0.1:3121") parser.add_argument("--log-file", help="write complete program_flash output here") parser.add_argument("--dry-run", action="store_true", help="run guards and print command only") args = parser.parse_args() image = Path(args.image).expanduser().resolve() jtag_fsbl = Path(args.jtag_fsbl).expanduser().resolve() for label, path in (("BOOT.bin", image), ("JTAG helper FSBL", jtag_fsbl)): if not path.is_file(): raise SystemExit(f"ERROR: missing {label}: {path}") if args.capacity_bytes <= 0: raise SystemExit("ERROR: --capacity-bytes must be positive") if not re.fullmatch(r"[0-9A-Fa-f]{64}", args.expect_sha256): raise SystemExit("ERROR: --expect-sha256 must contain exactly 64 hex digits") if (args.expect_jtag_fsbl_sha256 and not re.fullmatch(r"[0-9A-Fa-f]{64}", args.expect_jtag_fsbl_sha256)): raise SystemExit( "ERROR: --expect-jtag-fsbl-sha256 must contain exactly 64 hex digits" ) try: if int(args.offset, 0) != 0: raise ValueError except ValueError: raise SystemExit("ERROR: Zynq BOOT.bin must be programmed at offset 0") image_size = image.stat().st_size image_hash = sha256(image) expected_hash = args.expect_sha256.upper() if image_hash != expected_hash: raise SystemExit(f"ERROR: BOOT.bin SHA256 {image_hash} != expected {expected_hash}") if args.expect_size is not None and image_size != args.expect_size: raise SystemExit( f"ERROR: BOOT.bin size {image_size} != expected {args.expect_size}" ) if image_size >= args.capacity_bytes: raise SystemExit( f"ERROR: BOOT.bin size {image_size} is not below capacity {args.capacity_bytes}" ) try: marker_bytes = [(marker, marker.encode("ascii")) for marker in args.require_marker] except UnicodeEncodeError: raise SystemExit("ERROR: --require-marker values must be ASCII") image_bytes = image.read_bytes() missing = [marker for marker, raw in marker_bytes if raw not in image_bytes] if missing: raise SystemExit("ERROR: BOOT.bin lacks required markers: " + repr(missing)) helper_hash = sha256(jtag_fsbl) if (args.expect_jtag_fsbl_sha256 and helper_hash != args.expect_jtag_fsbl_sha256.upper()): raise SystemExit( f"ERROR: JTAG helper FSBL SHA256 {helper_hash} != expected " f"{args.expect_jtag_fsbl_sha256.upper()}" ) sdk_dir = Path(resolve_sdk_dir(args.sdk_dir)).expanduser().resolve() program_flash = sdk_dir / "bin" / "program_flash.bat" if not program_flash.is_file(): raise SystemExit(f"ERROR: program_flash not found: {program_flash}") cmd = [ str(program_flash), "-f", str(image), "-offset", args.offset, "-flash_type", args.flash_type, "-verify", "-fsbl", str(jtag_fsbl), "-cable", "type", "xilinx_tcf", "url", args.hw_server_url, ] print(f"[program_zynq_qspi] image : {image}") print(f"[program_zynq_qspi] size/sha256 : {image_size} / {image_hash}") print(f"[program_zynq_qspi] helper FSBL : {jtag_fsbl} / {helper_hash}") print(f"[program_zynq_qspi] flash/server: {args.flash_type} / {args.hw_server_url}") print("[program_zynq_qspi] command : " + subprocess.list2cmdline(cmd)) if args.dry_run: print("DRY_RUN_OK: guards passed; hardware was not touched") return host, port = parse_server(args.hw_server_url) try: with socket.create_connection((host, port), timeout=3): pass except OSError as exc: raise SystemExit(f"ERROR: hw_server is not reachable at {host}:{port}: {exc}") result = subprocess.run(cmd, capture_output=True, text=True, errors="replace") combined = (result.stdout or "") + (result.stderr or "") sys.stdout.write(combined) print(f"PROGRAM_FLASH_EXIT={result.returncode}") if args.log_file: log_path = Path(args.log_file).expanduser().resolve() log_path.parent.mkdir(parents=True, exist_ok=True) log_path.write_text(combined, encoding="utf-8", errors="replace") print(f"PROGRAM_FLASH_LOG={log_path}") required = [ "Erase Operation successful", "Program Operation successful", "Verify Operation successful", "Flash Operation Successful", ] missing_success = [marker for marker in required if marker not in combined] explicit_failure = re.search( r"(Verify Operation unsuccessful|Flash Operation Failed|Program Operation unsuccessful)", combined, re.IGNORECASE, ) if result.returncode != 0 or missing_success or explicit_failure: raise SystemExit( "ERROR: flash not proven successful; rc={} missing={} explicit_failure={}".format( result.returncode, missing_success, bool(explicit_failure) ) ) print("ZYNQ_QSPI_FLASH_VERIFY_PASS") if __name__ == "__main__": main()