Files
skills-win/fpga_flash/scripts/build_zynq_boot.py
T
boweiandClaude Opus 5 e07cfaa6d1 sync: WIN-BOWEI 2026-08-10 fpga_flash 增 Zynq BOOT.bin/QSPI 脚本,rtl-ip-replace 与 sim-rtl-flow 更新
- fpga_flash: 新增 build_zynq_boot.py / program_zynq_qspi.py(bootgen + program_flash,
  含备份、SHA-256/容量/marker 守卫);vivado_env.py 增 resolve_sdk_dir(SDK_ROOT/SDK_PATH);
  SKILL.md 补 Zynq 烧写流程,示例去掉写死的 D:\Xilinx\SDK\2018.1,改由 skills_env.md 解析。
- rtl-ip-replace: gen_ip.tcl / namespace_ip.py 更新。
- sim-rtl-flow: SKILL.md 与 run_sim_rtl_flow.py 更新。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 12:50:44 +08:00

169 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Build a guarded Zynq-7000 BOOT.bin from FSBL + PL bit + PS ELF.
The output is generated as a candidate, checked for capacity and required
ASCII build markers, then atomically promoted. Existing BOOT.bin/BIF files are
backed up before replacement. This script does not program hardware.
"""
import argparse
import datetime as dt
import hashlib
import json
import os
import shutil
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 require_file(label: str, value: str) -> Path:
path = Path(value).expanduser().resolve()
if not path.is_file():
raise SystemExit(f"ERROR: missing {label}: {path}")
return path
def bif_path(path: Path) -> str:
return path.as_posix()
def main() -> None:
parser = argparse.ArgumentParser(
description="Build guarded Zynq BOOT.bin (boot FSBL + PL bit + PS ELF)"
)
parser.add_argument("--sdk-dir", help="Xilinx SDK root containing bin/bootgen.bat")
parser.add_argument("--boot-fsbl", required=True, help="FSBL executed at cold boot")
parser.add_argument("--bit-file", required=True, help="implemented PL .bit file")
parser.add_argument("--app-elf", required=True, help="PS application ELF")
parser.add_argument("--boot-bin", required=True, help="final BOOT.bin path")
parser.add_argument("--bif-file", help="persistent BIF path; default beside BOOT.bin")
parser.add_argument("--capacity-bytes", type=int, default=32 * 1024 * 1024)
parser.add_argument(
"--require-marker", action="append", default=[],
help="ASCII marker that must exist in the generated image; repeatable",
)
parser.add_argument(
"--backup-dir",
help="backup root; default <BOOT.bin directory>/flash_backups",
)
parser.add_argument("--dry-run", action="store_true", help="validate and print only")
args = parser.parse_args()
boot_fsbl = require_file("boot FSBL", args.boot_fsbl)
bit_file = require_file("PL bitstream", args.bit_file)
app_elf = require_file("PS application ELF", args.app_elf)
boot_bin = Path(args.boot_bin).expanduser().resolve()
bif_file = (
Path(args.bif_file).expanduser().resolve()
if args.bif_file else boot_bin.with_suffix(".bif")
)
if args.capacity_bytes <= 0:
raise SystemExit("ERROR: --capacity-bytes must be positive")
try:
marker_bytes = [(marker, marker.encode("ascii")) for marker in args.require_marker]
except UnicodeEncodeError:
raise SystemExit("ERROR: --require-marker values must be ASCII")
app_bytes = app_elf.read_bytes()
missing_app_markers = [marker for marker, raw in marker_bytes if raw not in app_bytes]
if missing_app_markers:
raise SystemExit(
"ERROR: PS application ELF lacks required markers: " + repr(missing_app_markers)
)
sdk_dir = Path(resolve_sdk_dir(args.sdk_dir)).expanduser().resolve()
bootgen = sdk_dir / "bin" / "bootgen.bat"
if not bootgen.is_file():
raise SystemExit(f"ERROR: bootgen not found: {bootgen}")
bif_text = (
"the_ROM_image:\n"
"{\n"
f" [bootloader]{bif_path(boot_fsbl)}\n"
f" {bif_path(bit_file)}\n"
f" {bif_path(app_elf)}\n"
"}\n"
)
candidate = boot_bin.with_name(f"{boot_bin.name}.candidate.{os.getpid()}")
cmd = [
str(bootgen), "-image", str(bif_file), "-arch", "zynq",
"-o", str(candidate), "-w", "on",
]
print(f"[build_zynq_boot] boot FSBL: {boot_fsbl} sha256={sha256(boot_fsbl)}")
print(f"[build_zynq_boot] PL bit : {bit_file} sha256={sha256(bit_file)}")
print(f"[build_zynq_boot] PS ELF : {app_elf} sha256={sha256(app_elf)}")
print(f"[build_zynq_boot] output : {boot_bin}")
print(f"[build_zynq_boot] capacity : {args.capacity_bytes}")
print("[build_zynq_boot] command : " + subprocess.list2cmdline(cmd))
if args.dry_run:
print("DRY_RUN_OK: no files changed and bootgen was not executed")
return
boot_bin.parent.mkdir(parents=True, exist_ok=True)
bif_file.parent.mkdir(parents=True, exist_ok=True)
stamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
backup_root = (
Path(args.backup_dir).expanduser().resolve()
if args.backup_dir else boot_bin.parent / "flash_backups"
)
existing = [path for path in (boot_bin, bif_file) if path.exists()]
if existing:
backup = backup_root / stamp
backup.mkdir(parents=True, exist_ok=False)
for path in existing:
shutil.copy2(path, backup / path.name)
print(f"[build_zynq_boot] backup : {backup}")
bif_file.write_text(bif_text, encoding="utf-8", newline="\n")
try:
result = subprocess.run(cmd, check=False)
if result.returncode != 0 or not candidate.is_file():
raise RuntimeError(f"bootgen failed, rc={result.returncode}")
size = candidate.stat().st_size
if size >= args.capacity_bytes:
raise RuntimeError(
f"candidate size {size} is not below capacity {args.capacity_bytes}"
)
image = candidate.read_bytes()
missing = [marker for marker, raw in marker_bytes if raw not in image]
if missing:
raise RuntimeError("candidate lacks required markers: " + repr(missing))
candidate_hash = sha256(candidate)
os.replace(candidate, boot_bin)
except Exception as exc:
if candidate.exists():
candidate.unlink()
raise SystemExit(f"ERROR: {exc}")
manifest = {
"format": "zynq-7000-boot-bin",
"created": dt.datetime.now().astimezone().isoformat(),
"capacity_bytes": args.capacity_bytes,
"required_markers": args.require_marker,
"components": {
"boot_fsbl": {"path": str(boot_fsbl), "sha256": sha256(boot_fsbl)},
"pl_bit": {"path": str(bit_file), "sha256": sha256(bit_file)},
"ps_elf": {"path": str(app_elf), "sha256": sha256(app_elf)},
},
"boot_bin": {"path": str(boot_bin), "size": size, "sha256": candidate_hash},
}
manifest_path = boot_bin.with_suffix(boot_bin.suffix + ".manifest.json")
manifest_path.write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
print(f"ZYNQ_BOOTGEN_PASS size={size} sha256={candidate_hash}")
print(f"MANIFEST={manifest_path}")
if __name__ == "__main__":
main()