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 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;失败则回退备份 |
+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: 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."
)
+53 -10
View File
@@ -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
+31 -4
View File
@@ -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
View File
@@ -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
+37 -1
View File
@@ -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)