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
+154 -5
View File
@@ -1,13 +1,13 @@
---
---
name: fpga_flash
description: |
烧写 Artix-7 系列 FPGA 的外部 Flash(SPI NOR Flash)。适用于 EMV7.1 等 Artix-7 板卡,支持 Micron mt25ql128 和 Winbond w25q128bv 两种 Flash 型号。
按开发板/器件架构选择正确的 Flash 固化方式。支持 EMV7.1 等 Artix-7 板卡的 MCS/cfgmem 流程,以及 HJX9009(XC7Z100/Zynq-7000)将 FSBL、PL bitstream、PS ELF 打包为 BOOT.bin 后写入双并行 QSPI 的流程。
当用户提到"烧写 flash"、"flash 编程"、"固化 bit 文件"、"下载到 flash"、"program flash"、"write flash"、"burn flash"、"Vivado flash"、"SPI flash 烧写"时,必须触发此 skill。
---
# FPGA Flash 烧写流程(Artix-7 / EMV7.1)
# FPGA Flash 固化流程(按开发板路由)
本 skill 由 Claude **全自动执行**完整的 Flash 烧写流程。除了向用户确认必要信息外,所有操作(文件检查、CSV 修改、Vivado 命令执行、硬件编程)均由 Claude 直接完成,不要求用户手动执行任何命令。
本 skill 由 Claude **全自动执行**完整的 Flash 固化流程。除了向用户确认必要信息外,所有操作(文件检查、备份、哈希/容量/内容守卫、工具调用、硬件编程)均由 Claude 直接完成,不要求用户手动执行任何命令。执行前必须先识别开发板与 FPGA/SoC 类型;不得把 Artix-7 的 `.mcs/cfgmem` 流程用于 Zynq,也不得把 Zynq 的 `BOOT.bin/program_flash` 流程用于普通 Artix-7。
---
@@ -34,6 +34,9 @@ scripts/
program_flash.py # 第四、五步:连接目标板并烧写 Flash
step3_write_mcs.tcl # gen_mcs.py 使用的 Tcl 模板(勿手动修改)
step4_5_program.tcl # program_flash.py 使用的 Tcl 模板(勿手动修改)
build_zynq_boot.py # Zynq:FSBL + bit + PS ELF 生成受保护的 BOOT.bin
program_zynq_qspi.py # Zynq:哈希/容量/marker 守卫后烧写并强制 verify
vivado_env.py # 解析 Vivado/SDK 工具路径
```
**调用方式(Bash 工具):**
@@ -41,10 +44,153 @@ scripts/
python "$USERPROFILE/.claude/skills/fpga_flash/scripts/<script>.py" [参数...]
```
各脚本通过命令行参数接受变量(Vivado 安装目录、工程名、bit 文件路径、mcs 文件路径等),内部调用 Vivado `-mode batch -source <tcl模板> -tclargs <参数>` 完成执行,全程不产生临时文件。
各脚本通过命令行参数接受变量(Vivado/SDK 安装目录、bit/ELF/MCS/BOOT 路径、容量和 marker 等)。Artix-7 脚本内部调用 Vivado `-mode batch -source <tcl模板> -tclargs <参数>`;Zynq 脚本调用 SDK 的 `bootgen`/`program_flash`。工程路径和板级文件均由参数传入,不在 skill 中硬编码。
---
## 第零步:识别板卡并选择唯一流程
先用工程名、器件型号、原理图/板卡说明和现有部署资料交叉确认,不允许只根据输出文件扩展名猜测。
| 板卡/器件 | 固化容器 | 写入工具与接口 | 选择的流程 |
|---|---|---|---|
| HJX9009、`xc7z100`、Zynq-7000(PS+PL) | `BOOT.bin`,顺序为启动 FSBL + PL `.bit` + PS `.elf` | SDK `bootgen` + `program_flash -flash_type qspi_dual_parallel` | 本文“Zynq/HJX9009”流程 |
| EMV7.1、`xc7a*`、Artix-7(纯 PL) | `.mcs` | Vivado `write_cfgmem` + Hardware Manager cfgmem | 本文“Artix-7/EMV7.1”流程 |
硬阻断规则:
- 发现 `xc7z*`、PS7、FSBL、PS ELF 或工程明确写明 Zynq 时,禁止运行 `gen_mcs.py`、`program_flash.py` 或 `create_hw_cfgmem`。
- 发现 `xc7a*` 且无 PS/FSBL 时,禁止生成 `BOOT.bin`。
- 板型、Flash 拓扑或容量不明确时,先查原理图/现有已验证部署记录;不得试烧。
- HJX9009 已验证为两片 `s25fl128s` 双并行 QSPI,总容量 32 MiB,必须使用 `qspi_dual_parallel`,不能退化为 `qspi_single`。
---
## Zynq/HJX9009:同时固化 PS 与 PL
### Z1. 输入与两个 FSBL 角色
生成冷启动镜像需要三个输入,BIF 中顺序不可改变:
```text
the_ROM_image:
{
[bootloader]<boot_fsbl.elf>
<implemented_pl.bit>
<ps_application.elf>
}
```
必须区分两个角色不同的 FSBL:
- **启动 FSBL**:封装在 `BOOT.bin` 第一分区,板卡冷启动时由 BootROM 加载,随后配置 PL 并启动 PS 应用。
- **JTAG 烧写辅助 FSBL**:由 `program_flash -fsbl` 临时下载到 OCM,用于初始化当前板卡的 QSPI MIO/控制器并完成写入。它不等于、也不能默认替换 `BOOT.bin` 内的启动 FSBL。
对 HJX9009,必须使用已验证能初始化该板 QSPI MIO 的参考 JTAG 辅助 FSBL。若项目提供其期望 SHA-256/MD5,必须作为守卫验证。错误的辅助 FSBL 可能读出 JEDEC `00,00,00`,造成表面执行但没有可靠写入。
HJX9009 当前工程部署记录中的参考辅助 FSBL 指纹为 MD5 `14232757864453B418E67D63457EE24F`。这只是板级身份参考,不应写死进通用脚本;实际执行仍应从当前项目部署清单取得文件并同时记录 SHA-256。
### Z2. 安全预检与备份顺序
执行顺序固定如下:
1. 确认本次 PS ELF、时序通过的 PL bitstream、启动 FSBL 和 JTAG 辅助 FSBL 的绝对路径。
2. 记录四个输入的 SHA-256、字节数和修改时间;若有 `.ltx`,另记录其 SHA-256,确保它与 bitstream 同次构建。
3. 在覆盖部署目录中的任何已有 `BOOT.bin`、BIF、FSBL、bit 或 ELF 前,复制到带时间戳的备份目录并写 manifest。不得删除已知可回退镜像。
4. 对源文件和 staging 副本逐个比较 SHA-256,任一不一致立即停止。
5. 为新固件指定至少一个能区分版本的 ASCII marker,并在 PS ELF 和生成的 `BOOT.bin` 中检查。marker 不匹配立即停止。
6. 生成后要求 `BOOT.bin` 严格小于物理 QSPI 容量;HJX9009 上限为 33,554,432 字节。
7. 记录最终 `BOOT.bin` 的 SHA-256 和精确长度;烧写命令必须重新校验这两个值,防止旧镜像或 TOCTOU 误烧。
### Z3. 生成 BOOT.bin
调用参数化脚本;示例只表示参数结构,实际路径和 marker 必须来自当前项目。
Xilinx SDK 安装根目录由脚本自行解析(`SDK_ROOT` / `SDK_PATH`:先查环境变量,再查
`~\skills_env.md`),只有需要临时覆盖时才显式传 `--sdk-dir`:
```powershell
python "$env:USERPROFILE\.claude\skills\fpga_flash\scripts\build_zynq_boot.py" `
--boot-fsbl "<cold-boot-fsbl.elf>" `
--bit-file "<implemented-top.bit>" `
--app-elf "<ps-application.elf>" `
--boot-bin "<deployment-dir>\BOOT.bin" `
--bif-file "<deployment-dir>\boot.bif" `
--capacity-bytes 33554432 `
--require-marker "<build-identity-marker>"
```
脚本会备份已有 BOOT/BIF、生成候选镜像、执行容量与 marker 守卫、原子替换最终镜像,并输出 JSON manifest。成功标志为:
```text
ZYNQ_BOOTGEN_PASS size=<n> sha256=<64 hex>
```
如果工程采用 staging 目录,复制 FSBL/bit/ELF 后仍要逐项重算并对比 SHA-256。`BOOT.bin` 约 18 MiB 只是 HJX9009 的常见值,不是成功判据。
### Z4. 启动 Vivado hw_server 并确认 JTAG
HJX9009 当前机器上必须由 **Vivado** 的 `hw_server` 给 Platform Cable USB II 加载固件;SDK 自带 hw_server 可能无法完成该动作。先启动并等待枚举出 `xc7z100`:
```powershell
& '<Vivado-root>\bin\hw_server.bat' -s tcp::3121
```
保持服务运行,确认 `tcp:127.0.0.1:3121` 可连接且 JTAG 链中只有预期目标。若 3121 被未知进程占用、目标器件不是 `xc7z100`、板卡掉电或线缆未枚举,停止,不得烧写。
### Z5. 写入 HJX9009 双并行 QSPI 并强制 verify
从 Z3 的输出/manifest 复制**精确** SHA-256 和长度后执行:
```powershell
python "$env:USERPROFILE\.claude\skills\fpga_flash\scripts\program_zynq_qspi.py" `
--image "<deployment-dir>\BOOT.bin" `
--jtag-fsbl "<verified-jtag-helper-fsbl.elf>" `
--expect-sha256 "<exact-BOOT.bin-sha256>" `
--expect-size <exact-byte-length> `
--capacity-bytes 33554432 `
--flash-type qspi_dual_parallel `
--hw-server-url tcp:127.0.0.1:3121 `
--expect-jtag-fsbl-sha256 "<exact-helper-FSBL-sha256>" `
--require-marker "<build-identity-marker>" `
--log-file "<deployment-dir>\program_flash.log"
```
安全约束:
- offset 必须为 `0`;HJX9009 的 Flash 类型必须保持 `qspi_dual_parallel`。
- 参数顺序必须是 `-verify` 在 `-fsbl` 前,`-cable ...` 最后;参数化脚本已固定此顺序。
- 擦除、编程、回读校验可能超过 10 分钟;期间禁止断电、拔 JTAG、关闭 hw_server 或终止进程。
- 不信任包装层退出码。已验证存在“无法连接 hw_server 但返回码仍为 0”的情况。
真正成功必须在同一次日志中同时出现:
```text
Erase Operation successful
Program Operation successful
Verify Operation successful
Flash Operation Successful
ZYNQ_QSPI_FLASH_VERIFY_PASS
```
若出现 `Verify Operation unsuccessful`、`Flash Operation Failed`,或缺少任一成功行,一律判失败。`End of startup status: HIGH` 只证明易失配置完成,不能证明 Flash 已写入。
### Z6. 完全断电冷启动与 COM5 验收
烧写成功后还必须验证自主启动:
1. 保存烧写日志、BOOT manifest、bit/LTX/ELF 哈希和回退镜像位置。
2. 关闭板卡主电源及可能通过 JTAG 反向供电的接口,静置至少 30 秒;JTAG 枚举异常时按板级规程全断 90 秒。
3. 保持 JTAG 断开,重新打开主电源,让板卡只从 QSPI 启动。
4. 打开 COM5(115200、8N1、无流控),保存带时间戳的启动日志。
5. 要求看到本次 PS 固件的唯一 marker/banner,并执行项目定义的只读状态命令。HJX9009 当前固件通常用 `HELP`、`?`,随后按项目规程输入 `R`;验收期望状态和命令必须以本次固件文档为准。
6. 若 marker 不匹配、COM5 无输出、PL/PS 版本不一致或只能在重新接入 JTAG 后工作,判定冷启动失败并使用已备份的已知良好镜像回退。
只有“写入日志完整成功 + verify 成功 + 完全断电后 COM5 确认本次固件自主启动”三者同时成立,才可以向用户报告 HJX9009 PS+PL 已固化。
---
## Artix-7/EMV7.1:MCS/cfgmem 流程(保留)
## 第一步:检查并修复 Vivado Flash 器件表
**Claude 自动执行:**
@@ -156,3 +302,6 @@ ILA core 的 `Dropping logic core` WARNING 是正常现象(载入 loader/golde
| DONE 灯不亮 | 启动模式跳线错误 | 检查 MODE 引脚电平,确认为 SPI 启动模式 |
| 烧写后板卡跑旧程序 | Flash 未完全擦除 | 确认 `PROGRAM.ERASE 1` 已设置后重新执行 |
| `PermissionError` 写 CSV | 权限不足 | 提示用户以管理员身份运行 Claude Code |
| Zynq `program_flash` 返回 0 但无成功行 | hw_server 未连接或工具包装层假成功 | 按日志判失败;要求 erase/program/verify/Flash 四组成功标志齐全 |
| HJX9009 JEDEC 为 `00,00,00` | JTAG 辅助 FSBL 未正确初始化 QSPI MIO | 使用项目已验证的参考辅助 FSBL并核对哈希,保持 `qspi_dual_parallel` |
| `BOOT.bin` 可写但冷启动无 COM5 | 启动 FSBL/PS ELF/PL 不匹配或未自主启动 | 完全断电、断开 JTAG,从 QSPI 冷启动并核对固件 marker;失败则回退备份 |
+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."
)