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:
+154
-5
@@ -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;失败则回退备份 |
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user