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
+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()