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
|
name: fpga_flash
|
||||||
description: |
|
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。
|
当用户提到"烧写 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
|
program_flash.py # 第四、五步:连接目标板并烧写 Flash
|
||||||
step3_write_mcs.tcl # gen_mcs.py 使用的 Tcl 模板(勿手动修改)
|
step3_write_mcs.tcl # gen_mcs.py 使用的 Tcl 模板(勿手动修改)
|
||||||
step4_5_program.tcl # program_flash.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 工具):**
|
**调用方式(Bash 工具):**
|
||||||
@@ -41,10 +44,153 @@ scripts/
|
|||||||
python "$USERPROFILE/.claude/skills/fpga_flash/scripts/<script>.py" [参数...]
|
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 器件表
|
## 第一步:检查并修复 Vivado Flash 器件表
|
||||||
|
|
||||||
**Claude 自动执行:**
|
**Claude 自动执行:**
|
||||||
@@ -156,3 +302,6 @@ ILA core 的 `Dropping logic core` WARNING 是正常现象(载入 loader/golde
|
|||||||
| DONE 灯不亮 | 启动模式跳线错误 | 检查 MODE 引脚电平,确认为 SPI 启动模式 |
|
| DONE 灯不亮 | 启动模式跳线错误 | 检查 MODE 引脚电平,确认为 SPI 启动模式 |
|
||||||
| 烧写后板卡跑旧程序 | Flash 未完全擦除 | 确认 `PROGRAM.ERASE 1` 已设置后重新执行 |
|
| 烧写后板卡跑旧程序 | Flash 未完全擦除 | 确认 `PROGRAM.ERASE 1` 已设置后重新执行 |
|
||||||
| `PermissionError` 写 CSV | 权限不足 | 提示用户以管理员身份运行 Claude Code |
|
| `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:
|
if exe:
|
||||||
return exe
|
return exe
|
||||||
return os.path.join(resolve_vivado_dir(), "bin", "vivado.bat")
|
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."
|
||||||
|
)
|
||||||
|
|||||||
@@ -32,22 +32,58 @@ foreach ip $ips {
|
|||||||
}
|
}
|
||||||
switch -- $c(type) {
|
switch -- $c(type) {
|
||||||
dist_mem_gen {
|
dist_mem_gen {
|
||||||
|
# mode {rom} (default) -- constant lookup table, asynchronous read.
|
||||||
|
# mode {dual_port_ram} -- LUTRAM, port A = write + async read (spo),
|
||||||
|
# port B = async read (dpo), NO output registers. Use this (not
|
||||||
|
# blk_mem_gen) whenever the generated RAM needs a DEFINED read of
|
||||||
|
# the pre-write word while the other port writes the same address:
|
||||||
|
# a block-RAM primitive leaves the opposite port's read data
|
||||||
|
# undefined on such a collision (PG058), distributed RAM does not.
|
||||||
|
# The output registers / write-first read-back stay in the RTL
|
||||||
|
# wrapper so latency and enb gating remain cycle-identical.
|
||||||
|
set dmode [opt c mode {rom}]
|
||||||
create_ip -name dist_mem_gen -vendor xilinx.com -library ip -version 8.0 \
|
create_ip -name dist_mem_gen -vendor xilinx.com -library ip -version 8.0 \
|
||||||
-module_name $name -dir $ipdir
|
-module_name $name -dir $ipdir
|
||||||
set_property -dict [list \
|
if {$dmode eq "dual_port_ram"} {
|
||||||
CONFIG.memory_type {rom} \
|
set_property -dict [list \
|
||||||
CONFIG.data_width $c(width) \
|
CONFIG.memory_type {dual_port_ram} \
|
||||||
CONFIG.depth $c(depth) \
|
CONFIG.data_width $c(width) \
|
||||||
CONFIG.coefficient_file $c(coe) \
|
CONFIG.depth $c(depth) \
|
||||||
] [get_ips $name]
|
CONFIG.input_options {non_registered} \
|
||||||
|
CONFIG.output_options {non_registered} \
|
||||||
|
CONFIG.dual_port_address {non_registered} \
|
||||||
|
CONFIG.input_clock_enable {false} \
|
||||||
|
CONFIG.default_data {0} \
|
||||||
|
] [get_ips $name]
|
||||||
|
} else {
|
||||||
|
set_property -dict [list \
|
||||||
|
CONFIG.memory_type {rom} \
|
||||||
|
CONFIG.data_width $c(width) \
|
||||||
|
CONFIG.depth $c(depth) \
|
||||||
|
CONFIG.coefficient_file $c(coe) \
|
||||||
|
] [get_ips $name]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
blk_mem_gen {
|
blk_mem_gen {
|
||||||
# SDP, common clock, READ_FIRST on the write port, no output register,
|
# mode {sdp_readfirst_commonclk} (default): SDP, common clock,
|
||||||
# ENA/ENB pins -- matches SimpleDualPortRAM_generic semantics exactly.
|
# READ_FIRST on the write port, no output register, ENA/ENB pins --
|
||||||
|
# matches SimpleDualPortRAM_generic semantics exactly.
|
||||||
|
# mode {tdp_writefirst_commonclk}: TRUE dual port, port A
|
||||||
|
# WRITE_FIRST (write with read-back), port B READ_FIRST.
|
||||||
|
# CAUTION: a BRAM primitive does NOT define the port-B read data
|
||||||
|
# when port A writes the same address in the same cycle; only use
|
||||||
|
# this when the design provably never collides.
|
||||||
|
set bmode [opt c mode {sdp_readfirst_commonclk}]
|
||||||
|
set mtype {Simple_Dual_Port_RAM}
|
||||||
|
set opa {READ_FIRST}
|
||||||
|
if {$bmode eq "tdp_writefirst_commonclk"} {
|
||||||
|
set mtype {True_Dual_Port_RAM}
|
||||||
|
set opa {WRITE_FIRST}
|
||||||
|
}
|
||||||
create_ip -name blk_mem_gen -vendor xilinx.com -library ip -version 8.4 \
|
create_ip -name blk_mem_gen -vendor xilinx.com -library ip -version 8.4 \
|
||||||
-module_name $name -dir $ipdir
|
-module_name $name -dir $ipdir
|
||||||
set_property -dict [list \
|
set_property -dict [list \
|
||||||
CONFIG.Memory_Type {Simple_Dual_Port_RAM} \
|
CONFIG.Memory_Type $mtype \
|
||||||
CONFIG.Assume_Synchronous_Clk {true} \
|
CONFIG.Assume_Synchronous_Clk {true} \
|
||||||
CONFIG.Use_Byte_Write_Enable {false} \
|
CONFIG.Use_Byte_Write_Enable {false} \
|
||||||
CONFIG.Write_Width_A $c(width) \
|
CONFIG.Write_Width_A $c(width) \
|
||||||
@@ -55,12 +91,19 @@ foreach ip $ips {
|
|||||||
CONFIG.Read_Width_A $c(width) \
|
CONFIG.Read_Width_A $c(width) \
|
||||||
CONFIG.Write_Width_B $c(width) \
|
CONFIG.Write_Width_B $c(width) \
|
||||||
CONFIG.Read_Width_B $c(width) \
|
CONFIG.Read_Width_B $c(width) \
|
||||||
CONFIG.Operating_Mode_A {READ_FIRST} \
|
CONFIG.Operating_Mode_A $opa \
|
||||||
CONFIG.Enable_A {Use_ENA_Pin} \
|
CONFIG.Enable_A {Use_ENA_Pin} \
|
||||||
CONFIG.Enable_B {Use_ENB_Pin} \
|
CONFIG.Enable_B {Use_ENB_Pin} \
|
||||||
CONFIG.Register_PortB_Output_of_Memory_Primitives {false} \
|
CONFIG.Register_PortB_Output_of_Memory_Primitives {false} \
|
||||||
CONFIG.Register_PortB_Output_of_Memory_Core {false} \
|
CONFIG.Register_PortB_Output_of_Memory_Core {false} \
|
||||||
] [get_ips $name]
|
] [get_ips $name]
|
||||||
|
if {$bmode eq "tdp_writefirst_commonclk"} {
|
||||||
|
set_property -dict [list \
|
||||||
|
CONFIG.Operating_Mode_B {READ_FIRST} \
|
||||||
|
CONFIG.Register_PortA_Output_of_Memory_Primitives {false} \
|
||||||
|
CONFIG.Register_PortA_Output_of_Memory_Core {false} \
|
||||||
|
] [get_ips $name]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fifo_generator {
|
fifo_generator {
|
||||||
# guided: mode field carries standard|fwft; depth/width from manifest
|
# guided: mode field carries standard|fwft; depth/width from manifest
|
||||||
|
|||||||
@@ -8,7 +8,27 @@ coexist in one xsim `work` library for the dual-DUT cycle-true comparison.
|
|||||||
left untouched -- they are not declared here.
|
left untouched -- they are not declared here.
|
||||||
|
|
||||||
Run LAST, after rom_to_ip.py / ram_to_ip.py / any hand edits (CORDIC, FIFO).
|
Run LAST, after rom_to_ip.py / ram_to_ip.py / any hand edits (CORDIC, FIFO).
|
||||||
Idempotent: modules already ending in _IP are skipped.
|
Idempotent: a name already ending in _IP is never suffixed twice.
|
||||||
|
|
||||||
|
INCREMENTAL RUNS -- THE BUG THIS DOCSTRING USED TO DESCRIBE AS A FEATURE.
|
||||||
|
The rename set used to be "modules declared here whose name does NOT already end
|
||||||
|
in _IP", i.e. it treated an already-suffixed declaration as nothing left to do.
|
||||||
|
That is wrong the moment incr_plan.py restores previously-rewritten files and
|
||||||
|
drops ONE freshly regenerated file beside them, which is the normal incremental
|
||||||
|
case:
|
||||||
|
|
||||||
|
EncAddr.v restored from the last run -> declares EncAddr_IP
|
||||||
|
DpdTrain.v freshly generated -> declares DpdTrain, instantiates EncAddr
|
||||||
|
|
||||||
|
`EncAddr` was no longer in the rename set, so the new DpdTrain.v kept its bare
|
||||||
|
`EncAddr u_EncAddr (...)` while the file that defines it had been renamed away.
|
||||||
|
The delivery then elaborated only because the dual-DUT bench compiled the GOLDEN
|
||||||
|
tree into the same work library, where a bare `EncAddr` still exists; synthesis,
|
||||||
|
which sees the delivery alone, failed with "module 'EncAddr' not found".
|
||||||
|
|
||||||
|
The rename set is therefore built from the BASE NAME of every declaration --
|
||||||
|
X_IP counts as X -- so a regenerated file is namespaced against the whole
|
||||||
|
delivery, not just against the part of it that has not been namespaced yet.
|
||||||
"""
|
"""
|
||||||
import argparse, os, re
|
import argparse, os, re
|
||||||
|
|
||||||
@@ -19,13 +39,20 @@ def main():
|
|||||||
d = os.path.abspath(a.rtl_ip_dir)
|
d = os.path.abspath(a.rtl_ip_dir)
|
||||||
files = [os.path.join(d, f) for f in os.listdir(d) if f.endswith('.v')]
|
files = [os.path.join(d, f) for f in os.listdir(d) if f.endswith('.v')]
|
||||||
|
|
||||||
mods = []
|
mods, already = [], []
|
||||||
for p in files:
|
for p in files:
|
||||||
for m in re.finditer(r'^\s*module\s+(\w+)', open(p, encoding='utf-8', errors='replace').read(), re.M):
|
for m in re.finditer(r'^\s*module\s+(\w+)', open(p, encoding='utf-8', errors='replace').read(), re.M):
|
||||||
if not m.group(1).endswith('_IP'):
|
n = m.group(1)
|
||||||
mods.append(m.group(1))
|
if n.endswith('_IP'):
|
||||||
|
already.append(n[:-3])
|
||||||
|
mods.append(n[:-3]) # base name: a restored file still OWNS this name
|
||||||
|
else:
|
||||||
|
mods.append(n)
|
||||||
mods = sorted(set(mods), key=len, reverse=True)
|
mods = sorted(set(mods), key=len, reverse=True)
|
||||||
print('modules:', mods)
|
print('modules:', mods)
|
||||||
|
if already:
|
||||||
|
print('already namespaced (kept in the rename set as base names):',
|
||||||
|
sorted(set(already)))
|
||||||
|
|
||||||
for p in files:
|
for p in files:
|
||||||
txt = open(p, encoding='utf-8', errors='replace').read()
|
txt = open(p, encoding='utf-8', errors='replace').read()
|
||||||
|
|||||||
+83
-11
@@ -2,14 +2,18 @@
|
|||||||
name: sim-rtl-flow
|
name: sim-rtl-flow
|
||||||
description: >
|
description: >
|
||||||
Run the sim-rtl-flow LangGraph workflow at E:\LangGraph_Design\sim-rtl-flow — the
|
Run the sim-rtl-flow LangGraph workflow at E:\LangGraph_Design\sim-rtl-flow — the
|
||||||
Simulink-to-RTL-to-hardware design pipeline: Simulink -x principle sim (sim-golden) →
|
Simulink-to-RTL-to-hardware design pipeline: read the previous run's design history
|
||||||
HUMAN sign-off on simulation performance → optional -q fixed-point → parallel
|
doc sim-rtl-flow-<date>.md to decide what may be reused → Simulink -x principle sim
|
||||||
functional partition + RTL/SoC interface spec → HUMAN sign-off on the partition →
|
(sim-golden) → HUMAN sign-off on simulation performance → optional -q fixed-point →
|
||||||
per-module -E.slx nativization and Xilinx-IP replacement (parallel Send fanout) →
|
parallel functional partition + RTL/SoC interface spec → HUMAN sign-off on the
|
||||||
Vivado build + timing → hardware-in-the-loop via fpga-imx6-flow or zynq-imx6-flow.
|
partition (skippable when history says the partition is unchanged) → per-module
|
||||||
It drives the compiled graph in-process (no langgraph dev server) and hands every
|
-E.slx nativization and Xilinx-IP replacement (parallel Send fanout, modules the
|
||||||
subagent task back to THIS Claude session — and every human checkpoint to the USER —
|
history proved good are skipped) → Vivado build + timing → hardware-in-the-loop via
|
||||||
through a record/replay ledger stored in the design project being worked on.
|
fpga-imx6-flow or zynq-imx6-flow → write a fresh sim-rtl-flow-<date>.md summarising
|
||||||
|
every -x / -q / -E version. It drives the compiled graph in-process (no langgraph dev
|
||||||
|
server) and hands every subagent task back to THIS Claude session — and every human
|
||||||
|
checkpoint to the USER — through a record/replay ledger stored in the design project
|
||||||
|
being worked on.
|
||||||
|
|
||||||
ALWAYS use this skill whenever the user wants to run, start, drive, resume or continue
|
ALWAYS use this skill whenever the user wants to run, start, drive, resume or continue
|
||||||
the sim-rtl-flow workflow / Simulink→RTL design flow, or says things like "启动
|
the sim-rtl-flow workflow / Simulink→RTL design flow, or says things like "启动
|
||||||
@@ -22,7 +26,7 @@ description: >
|
|||||||
|
|
||||||
## What this skill does
|
## What this skill does
|
||||||
|
|
||||||
Drives the **sim-rtl-flow** graph (21 top-level nodes + a 3-node per-module subgraph,
|
Drives the **sim-rtl-flow** graph (25 top-level nodes + a 5-node per-module subgraph,
|
||||||
1:1 with `Notes/PR01-1.md`, `Notes/PR01-2.md` and `Notes/PR01_sim_rtl_flow.drawio`).
|
1:1 with `Notes/PR01-1.md`, `Notes/PR01-2.md` and `Notes/PR01_sim_rtl_flow.drawio`).
|
||||||
|
|
||||||
The graph contains **no** agent logic — it talks to the outside world through two
|
The graph contains **no** agent logic — it talks to the outside world through two
|
||||||
@@ -60,6 +64,50 @@ export PYTHONIOENCODING=utf-8
|
|||||||
The driver resolves the workflow checkout from `SIM_RTL_FLOW_ROOT` in
|
The driver resolves the workflow checkout from `SIM_RTL_FLOW_ROOT` in
|
||||||
`~\skills_env.md`, so no path is baked into this skill.
|
`~\skills_env.md`, so no path is baked into this skill.
|
||||||
|
|
||||||
|
## 📄 The design history doc `sim-rtl-flow-<date>.md`
|
||||||
|
|
||||||
|
Every run **reads** the newest history doc and **writes** a new one. That doc is the
|
||||||
|
only mechanism stopping the flow from re-doing — and thereby re-breaking — design work
|
||||||
|
that a previous run already validated.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Location | `<work_dir>/<history_dir>/`, where `history_dir` defaults to the folder holding `requirements_path` (usually `Notes`) |
|
||||||
|
| Name | `sim-rtl-flow-<YYYYMMDD-HH>.md` — date **and hour**, zero-padded, so name order == time order |
|
||||||
|
| Read | `start` freezes the newest matching file into `input.json` as `history_path`; the `history_review#1` task interprets it |
|
||||||
|
| Written | the `design_summary#1` task, at the end of **every** run — completed *and* failed |
|
||||||
|
|
||||||
|
`history_review` returns its verdict in `metadata`, and that verdict is what lets the
|
||||||
|
graph skip work:
|
||||||
|
|
||||||
|
| metadata field | effect if `false` / listed |
|
||||||
|
|---|---|
|
||||||
|
| `redo_fixed_point` | `false` → the `-q` stage is skipped entirely |
|
||||||
|
| `redo_partition` + `partition_review_required` | both `false` → the **`functional_partition` human checkpoint is skipped** |
|
||||||
|
| `reuse_modules` | listed module → its `native:M` + `ip_replace:M` are skipped; result recorded as `stage: history_reuse` |
|
||||||
|
| `redo_modules` | listed module always re-runs, even if also in `reuse_modules` |
|
||||||
|
|
||||||
|
**Every default is "redo".** Omit a field and the stage runs. Fail the
|
||||||
|
`history_review` gate (`gate_passed: false`) and *nothing* is skipped. Recording a skip
|
||||||
|
you did not verify means a later gate — Vivado, HIL, sim-golden — validates a design
|
||||||
|
nobody checked this run.
|
||||||
|
|
||||||
|
Four things force everything back to redo, regardless of the history doc:
|
||||||
|
|
||||||
|
1. `simulink_x` running a second time (a rejected `sim_performance`, or a retry) —
|
||||||
|
sim-golden may have moved, so the history describes a *different* `-x`;
|
||||||
|
2. `partition#n` returning `metadata.partition_changed: true` — the human checkpoint
|
||||||
|
comes back even if history said to skip it;
|
||||||
|
3. any module named by Vivado or HIL rework — the reuse pool is consumed after the
|
||||||
|
first module round and never applies to a rework round;
|
||||||
|
4. the user having already answered `functional_partition` once — a rejection can never
|
||||||
|
be swallowed by a later "history says skip it".
|
||||||
|
|
||||||
|
When you write the summary, `artifacts[0]` must be the real path you wrote. If the
|
||||||
|
summary cannot be written after `--max-attempts`, the run is reported **failed** even
|
||||||
|
if the design itself passed — because the next run would silently lose its basis for
|
||||||
|
skipping anything.
|
||||||
|
|
||||||
## ⚠️ The two human checkpoints are the user's decision, not yours
|
## ⚠️ The two human checkpoints are the user's decision, not yours
|
||||||
|
|
||||||
The graph stops at two points that **an agent may not decide**:
|
The graph stops at two points that **an agent may not decide**:
|
||||||
@@ -67,7 +115,7 @@ The graph stops at two points that **an agent may not decide**:
|
|||||||
| checkpoint | where | what the user confirms |
|
| checkpoint | where | what the user confirms |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `sim_performance` | after step 1 | the `-x` sim meets the requirement and may be frozen as **sim-golden** |
|
| `sim_performance` | after step 1 | the `-x` sim meets the requirement and may be frozen as **sim-golden** |
|
||||||
| `functional_partition` | after step 3 | the pure-sim / RTL / SoC split and the RTL↔SoC interface doc |
|
| `functional_partition` | after step 3 | the pure-sim / RTL / SoC split and the RTL↔SoC interface doc — **skipped** only when the history doc says the partition is unchanged (see above) |
|
||||||
|
|
||||||
When one comes up the driver prints a `!!!` banner. **Relay the question and its context
|
When one comes up the driver prints a `!!!` banner. **Relay the question and its context
|
||||||
to the user verbatim, then wait.** Do not approve on your own judgement, do not infer
|
to the user verbatim, then wait.** Do not approve on your own judgement, do not infer
|
||||||
@@ -109,6 +157,10 @@ From `Notes/PR01.md` and `Notes/PR01-2.md`; the graph routes on these.
|
|||||||
`fpga-imx6-flow` / `zynq-imx6-flow` for *diagnosis only*; the moment a code change is
|
`fpga-imx6-flow` / `zynq-imx6-flow` for *diagnosis only*; the moment a code change is
|
||||||
needed, stop that sub-flow and report it back here — the fix happens in the
|
needed, stop that sub-flow and report it back here — the fix happens in the
|
||||||
sim-rtl-flow main graph, then re-enters Vivado and HIL.
|
sim-rtl-flow main graph, then re-enters Vivado and HIL.
|
||||||
|
7. **Skipping a step needs written evidence, and every run leaves some.** The only
|
||||||
|
grounds for not executing a stage are a history doc that records it as verified and
|
||||||
|
unaffected. Every run ends by writing the next `sim-rtl-flow-<date>.md`; anything you
|
||||||
|
could not verify goes in as *not verified*, so the next run redoes it.
|
||||||
|
|
||||||
## Run (agent path)
|
## Run (agent path)
|
||||||
|
|
||||||
@@ -140,6 +192,8 @@ EOF
|
|||||||
| `fixed_point_required` | `false` skips the `-q` stage |
|
| `fixed_point_required` | `false` skips the `-q` stage |
|
||||||
| `rtl_modules` | submodule names; each gets its own parallel `native → ip_replace` chain. Non-empty. |
|
| `rtl_modules` | submodule names; each gets its own parallel `native → ip_replace` chain. Non-empty. |
|
||||||
| `soc_modules` | MCU/SoC-side modules (interface-spec input) |
|
| `soc_modules` | MCU/SoC-side modules (interface-spec input) |
|
||||||
|
| `history_dir` | optional — folder holding `sim-rtl-flow-<date>.md`; defaults to the folder of `requirements_path` |
|
||||||
|
| `history_path` | optional — pin one history doc. Omit and `start` fills in the newest it finds; set `""` to declare "no history, do everything". |
|
||||||
|
|
||||||
### 2. Start — exits 10 with a handoff
|
### 2. Start — exits 10 with a handoff
|
||||||
|
|
||||||
@@ -182,6 +236,7 @@ keys with a default success — rehearsal only, and it refuses review keys.
|
|||||||
|
|
||||||
| key | what you actually do | which skill to use |
|
| key | what you actually do | which skill to use |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| `history_review#n` | read `sim-rtl-flow-<date>.md` + the requirement doc, decide what may be reused; verdict in `metadata` (**first task of every run that has a history doc**) | inline — read the files, don't guess |
|
||||||
| `simulink_x#n` | build/revise the `-x` sim, document the performance envelope | MATLAB MCP + `model-based-design-core:building-simulink-models`, `simulating-simulink-models` |
|
| `simulink_x#n` | build/revise the `-x` sim, document the performance envelope | MATLAB MCP + `model-based-design-core:building-simulink-models`, `simulating-simulink-models` |
|
||||||
| `review:sim_performance#n` | **ask the user** to sign off on sim-golden | — (relay to user) |
|
| `review:sim_performance#n` | **ask the user** to sign off on sim-golden | — (relay to user) |
|
||||||
| `fixed_point#n` | `-q` fixed-point, compare to `-x` | same MATLAB skills |
|
| `fixed_point#n` | `-q` fixed-point, compare to `-x` | same MATLAB skills |
|
||||||
@@ -197,6 +252,7 @@ keys with a default success — rehearsal only, and it refuses review keys.
|
|||||||
| `hil:fpga-imx6-flow#n` / `hil:zynq-imx6-flow#n` | HIL debug, **read-only** | `fpga-imx6-flow` / `zynq-imx6-flow` |
|
| `hil:fpga-imx6-flow#n` / `hil:zynq-imx6-flow#n` | HIL debug, **read-only** | `fpga-imx6-flow` / `zynq-imx6-flow` |
|
||||||
| `timing_rework#n` | fix non-Simulink RTL / SoC / project timing | inline |
|
| `timing_rework#n` | fix non-Simulink RTL / SoC / project timing | inline |
|
||||||
| `hil_rework#n` | apply the code change HIL asked for | inline |
|
| `hil_rework#n` | apply the code change HIL asked for | inline |
|
||||||
|
| `design_summary#n` | write `<history_dir>/sim-rtl-flow-<YYYYMMDD-HH>.md` (path handed to you in `context.target_path`) covering every `-x`/`-q`/`-E` version, the partition, per-module status, Vivado/HIL verdicts, what was reused and what is still open; return the real path in `artifacts` (**last task of every run, success or failure**) | inline |
|
||||||
|
|
||||||
`#n` is the n-th time that key has been requested this run — retries and rework produce
|
`#n` is the n-th time that key has been requested this run — retries and rework produce
|
||||||
`#2`, `#3`, …
|
`#2`, `#3`, …
|
||||||
@@ -238,9 +294,12 @@ A failing `ip_replace:<M>` (cycle-true mismatch) enters the same chain on its ow
|
|||||||
| stage | keys handed over at once |
|
| stage | keys handed over at once |
|
||||||
|---|---|
|
|---|---|
|
||||||
| after `-x` / `-q` | `interfaces#1` **and** `partition#1` |
|
| after `-x` / `-q` | `interfaces#1` **and** `partition#1` |
|
||||||
| module fanout | one `native:<M>#1` per module (3 for `["A","B","C"]`) |
|
| module fanout | one `native:<M>#1` per module (3 for `["A","B","C"]`) — **minus any module reused from history** |
|
||||||
| next module stage | one `ip_replace:<M>#1` per module |
|
| next module stage | one `ip_replace:<M>#1` per module |
|
||||||
|
|
||||||
|
`history_review#1` and `design_summary#1` are always single-task batches, at the very
|
||||||
|
start and the very end of the run.
|
||||||
|
|
||||||
Work batched tasks in any order; record all, then `resume`.
|
Work batched tasks in any order; record all, then `resume`.
|
||||||
|
|
||||||
## Rehearsal without touching hardware
|
## Rehearsal without touching hardware
|
||||||
@@ -311,6 +370,19 @@ Global flags go **before** the subcommand: `--project`, `--work-dir`, `--thread-
|
|||||||
are fine on Windows; a relative `"."` resolves against wherever the driver was
|
are fine on Windows; a relative `"."` resolves against wherever the driver was
|
||||||
launched — always use an absolute path.
|
launched — always use an absolute path.
|
||||||
- **`start` wipes the ledger** unless `--keep-ledger`. Use a fresh `--thread-id` per run.
|
- **`start` wipes the ledger** unless `--keep-ledger`. Use a fresh `--thread-id` per run.
|
||||||
|
- **`start` freezes `history_path` into `input.json`.** It must not be re-scanned on
|
||||||
|
every `resume`: the run itself writes a *newer* `sim-rtl-flow-<date>.md` at the end,
|
||||||
|
and a re-scan would make a later replay reference a different history doc than the
|
||||||
|
one already recorded in the ledger. Pinning it also makes `resume` after
|
||||||
|
`WORKFLOW FINISHED` a no-op instead of a spurious new handoff.
|
||||||
|
- **A reused module produces no `native:`/`ip_replace:` keys at all.** If you expected
|
||||||
|
three modules and only got one task, check the `modules_reused_from_history` event —
|
||||||
|
that is the graph honouring your own `history_review` verdict, not a lost module.
|
||||||
|
- **`design_summary` is the deliverable, not a formality.** Record it only after the
|
||||||
|
file really exists on disk with real content; `artifacts[0]` must be its path.
|
||||||
|
- **The summary can fail an otherwise-good run.** `status: failed` with
|
||||||
|
`运行已结束但历史设计说明文件 … 未能生成` means the design work passed but the history
|
||||||
|
doc did not get written — fix that and re-run the summary, don't ignore it.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,10 @@ def _bootstrap(project: Path):
|
|||||||
SubagentResult,
|
SubagentResult,
|
||||||
)
|
)
|
||||||
from sim_rtl_flow.graph import WorkflowContext, build_workflow # noqa: E402
|
from sim_rtl_flow.graph import WorkflowContext, build_workflow # noqa: E402
|
||||||
|
from sim_rtl_flow.history import ( # noqa: E402
|
||||||
|
discover_history_doc,
|
||||||
|
history_dir_for,
|
||||||
|
)
|
||||||
from sim_rtl_flow.review import ReviewDecision, ScriptedReviewGate # noqa: E402
|
from sim_rtl_flow.review import ReviewDecision, ScriptedReviewGate # noqa: E402
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -128,6 +132,8 @@ def _bootstrap(project: Path):
|
|||||||
"ReviewDecision": ReviewDecision,
|
"ReviewDecision": ReviewDecision,
|
||||||
"WorkflowContext": WorkflowContext,
|
"WorkflowContext": WorkflowContext,
|
||||||
"build_workflow": build_workflow,
|
"build_workflow": build_workflow,
|
||||||
|
"discover_history_doc": discover_history_doc,
|
||||||
|
"history_dir_for": history_dir_for,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -395,7 +401,20 @@ def _print_final(state: Any, replayed: list[str]) -> None:
|
|||||||
review = state.get(key)
|
review = state.get(key)
|
||||||
if review:
|
if review:
|
||||||
mark = "批准" if review.get("approved") else "驳回"
|
mark = "批准" if review.get("approved") else "驳回"
|
||||||
|
if (review.get("metadata") or {}).get("skipped_by_history"):
|
||||||
|
mark = "按历史设计说明文件跳过"
|
||||||
print(f"{label} : {mark} {review.get('notes', '')}".rstrip())
|
print(f"{label} : {mark} {review.get('notes', '')}".rstrip())
|
||||||
|
plan = state.get("history_plan") or {}
|
||||||
|
print(f"历史文件 · 参考 : {plan.get('history_path') or '(none)'}")
|
||||||
|
if plan.get("history_path"):
|
||||||
|
print(
|
||||||
|
"历史文件 · 沿用 : "
|
||||||
|
f"重做定点化={plan.get('redo_fixed_point')} "
|
||||||
|
f"重做功能划分={plan.get('redo_partition')} "
|
||||||
|
f"沿用子模块={plan.get('reuse_modules')}"
|
||||||
|
)
|
||||||
|
written = state.get("history_doc_written")
|
||||||
|
print(f"历史文件 · 本次 : {written or '(未生成!下次运行将失去跳步依据)'}")
|
||||||
print(f"modules : {state.get('rtl_modules')}")
|
print(f"modules : {state.get('rtl_modules')}")
|
||||||
for item in state.get("module_results", []):
|
for item in state.get("module_results", []):
|
||||||
print(
|
print(
|
||||||
@@ -452,7 +471,20 @@ def _invoke(run: RunDir, api: dict[str, Any], args) -> int:
|
|||||||
def cmd_start(args, api) -> int:
|
def cmd_start(args, api) -> int:
|
||||||
payload = json.loads(Path(args.input).read_text(encoding="utf-8"))
|
payload = json.loads(Path(args.input).read_text(encoding="utf-8"))
|
||||||
payload.pop("outcomes", None)
|
payload.pop("outcomes", None)
|
||||||
run = RunDir(_resolve_work_dir(args, payload), args.thread_id)
|
payload.pop("reviews", None)
|
||||||
|
work_dir = _resolve_work_dir(args, payload)
|
||||||
|
# 把"参考哪一份历史设计说明文件"冻结进 input.json:本次运行结束时会写出一份
|
||||||
|
# 新的 sim-rtl-flow-<date>.md,若每次 resume 都重新扫描目录,replay 就会在
|
||||||
|
# 半路换掉参考文件,走出一条与已记录 ledger 不同的路。
|
||||||
|
if "history_path" not in payload:
|
||||||
|
history_dir = api["history_dir_for"](
|
||||||
|
payload.get("history_dir"), payload.get("requirements_path", "")
|
||||||
|
)
|
||||||
|
payload["history_dir"] = history_dir
|
||||||
|
payload["history_path"] = api["discover_history_doc"](
|
||||||
|
str(work_dir), history_dir
|
||||||
|
)
|
||||||
|
run = RunDir(work_dir, args.thread_id)
|
||||||
run.set_input(payload)
|
run.set_input(payload)
|
||||||
if not args.keep_ledger:
|
if not args.keep_ledger:
|
||||||
run.set_ledger({})
|
run.set_ledger({})
|
||||||
@@ -460,6 +492,10 @@ def cmd_start(args, api) -> int:
|
|||||||
print(f"work dir : {run.work_dir}")
|
print(f"work dir : {run.work_dir}")
|
||||||
print(f"run dir : {run.path}")
|
print(f"run dir : {run.path}")
|
||||||
print(f"thread : {run.thread_id}")
|
print(f"thread : {run.thread_id}")
|
||||||
|
print(
|
||||||
|
"history : "
|
||||||
|
+ (payload.get("history_path") or "(none — 首次设计,全部步骤实打实执行)")
|
||||||
|
)
|
||||||
print("input : " + json.dumps(payload, ensure_ascii=False))
|
print("input : " + json.dumps(payload, ensure_ascii=False))
|
||||||
return _invoke(run, api, args)
|
return _invoke(run, api, args)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user