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>
This commit is contained in:
bowei
2026-08-19 12:50:44 +08:00
co-authored by Claude Opus 5
parent 7a11bac29d
commit e07cfaa6d1
8 changed files with 712 additions and 31 deletions
+168
View File
@@ -0,0 +1,168 @@
#!/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()
+166
View File
@@ -0,0 +1,166 @@
#!/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()
+20
View File
@@ -56,3 +56,23 @@ def resolve_vivado_exe(cli_value=None):
if exe:
return exe
return os.path.join(resolve_vivado_dir(), "bin", "vivado.bat")
def resolve_sdk_dir(cli_value=None):
"""Xilinx SDK install ROOT (the dir containing bin/bootgen.bat).
Precedence matches the Vivado resolver: explicit CLI value, SDK_ROOT,
SDK_PATH, then the same keys in ~/skills_env.md.
"""
if cli_value:
return cli_value
root = env_lookup("SDK_ROOT")
if root:
return root
exe = env_lookup("SDK_PATH")
if exe:
return os.path.dirname(os.path.dirname(exe))
sys.exit(
"Xilinx SDK path not configured. Add `SDK_ROOT = <install root>` to "
"~/skills_env.md, set it as an env var, or pass --sdk-dir."
)