Compare commits
8
Commits
7a11bac29d
...
120dc66925
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
120dc66925 | ||
|
|
307bc38d36 | ||
|
|
28307b12f6 | ||
|
|
effa9361ac | ||
|
|
6535dd83d4 | ||
|
|
d77d025c59 | ||
|
|
0b5583a5c0 | ||
|
|
e07cfaa6d1 |
Binary file not shown.
@@ -18,14 +18,42 @@ removed). CLI + exit codes:
|
||||
Exit codes: 0 complete · 1 error · 2 interrupted (human action needed)
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import uuid
|
||||
import argparse
|
||||
import sqlite3
|
||||
|
||||
|
||||
def _env_lookup(key, default=None):
|
||||
"""Read KEY from the OS environment, else from a ``KEY = value`` line in
|
||||
~/skills_env.md. Machine-local paths are never hardcoded in a skill; see
|
||||
the same helper in fpga_flash/scripts/vivado_env.py."""
|
||||
value = os.environ.get(key)
|
||||
if value:
|
||||
return value
|
||||
path = os.path.join(os.path.expanduser("~"), "skills_env.md")
|
||||
try:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
match = re.match(rf"\s*{re.escape(key)}\s*=\s*(.+?)\s*(?:#.*)?$", line)
|
||||
if match:
|
||||
found = match.group(1).strip()
|
||||
if found and not found.startswith("<"):
|
||||
return found
|
||||
except OSError:
|
||||
pass
|
||||
return default
|
||||
|
||||
|
||||
# Drive the graph in-process: import the app package.
|
||||
FPGA_FLOW_ROOT = r"E:\LangGraph_Design\fpga-goldenref-flow"
|
||||
FPGA_FLOW_ROOT = _env_lookup("FPGA_GOLDENREF_ROOT")
|
||||
if not FPGA_FLOW_ROOT:
|
||||
raise SystemExit(
|
||||
"fpga-goldenref-flow 工作流仓库路径未配置:请在 ~/skills_env.md 增加\n"
|
||||
" FPGA_GOLDENREF_ROOT = <fpga-goldenref-flow 检出目录>"
|
||||
)
|
||||
if FPGA_FLOW_ROOT not in sys.path:
|
||||
sys.path.insert(0, FPGA_FLOW_ROOT)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -27,7 +28,30 @@ try:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
DEFAULT_PROJECT = r"E:\LangGraph_Design\FPGA-imx6-flow"
|
||||
|
||||
def _env_lookup(key: str, default: str | None = None) -> str | None:
|
||||
"""Read KEY from the OS environment, else from a ``KEY = value`` line in
|
||||
~/skills_env.md. Machine-local paths are never hardcoded in a skill; see
|
||||
the same helper in fpga_flash/scripts/vivado_env.py."""
|
||||
value = os.environ.get(key)
|
||||
if value:
|
||||
return value
|
||||
path = os.path.join(os.path.expanduser("~"), "skills_env.md")
|
||||
try:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
match = re.match(rf"\s*{re.escape(key)}\s*=\s*(.+?)\s*(?:#.*)?$", line)
|
||||
if match:
|
||||
found = match.group(1).strip()
|
||||
if found and not found.startswith("<"):
|
||||
return found
|
||||
except OSError:
|
||||
pass
|
||||
return default
|
||||
|
||||
|
||||
# The fpga-imx6-flow checkout: FPGA_IMX6_FLOW_ROOT in ~/skills_env.md.
|
||||
DEFAULT_PROJECT = _env_lookup("FPGA_IMX6_FLOW_ROOT", "")
|
||||
|
||||
|
||||
def _emit(obj: dict, code: int) -> None:
|
||||
@@ -57,6 +81,11 @@ def main() -> None:
|
||||
ap.add_argument("--max-hw-retry", type=int, default=3)
|
||||
args = ap.parse_args()
|
||||
|
||||
if not args.project_dir:
|
||||
_emit({"status": "error",
|
||||
"error": "fpga-imx6-flow 工作流仓库路径未配置:在 ~/skills_env.md 增加 "
|
||||
"FPGA_IMX6_FLOW_ROOT = <检出目录>,或用 --project-dir 显式指定"}, 1)
|
||||
|
||||
project = Path(args.project_dir)
|
||||
if not (project / "app" / "main_graph.py").exists():
|
||||
_emit({"status": "error",
|
||||
|
||||
+154
-5
@@ -1,13 +1,13 @@
|
||||
---
|
||||
---
|
||||
name: fpga_flash
|
||||
description: |
|
||||
烧写 Artix-7 系列 FPGA 的外部 Flash(SPI NOR Flash)。适用于 EMV7.1 等 Artix-7 板卡,支持 Micron mt25ql128 和 Winbond w25q128bv 两种 Flash 型号。
|
||||
按开发板/器件架构选择正确的 Flash 固化方式。支持 EMV7.1 等 Artix-7 板卡的 MCS/cfgmem 流程,以及 HJX9009(XC7Z100/Zynq-7000)将 FSBL、PL bitstream、PS ELF 打包为 BOOT.bin 后写入双并行 QSPI 的流程。
|
||||
当用户提到"烧写 flash"、"flash 编程"、"固化 bit 文件"、"下载到 flash"、"program flash"、"write flash"、"burn flash"、"Vivado flash"、"SPI flash 烧写"时,必须触发此 skill。
|
||||
---
|
||||
|
||||
# FPGA Flash 烧写流程(Artix-7 / EMV7.1)
|
||||
# FPGA Flash 固化流程(按开发板路由)
|
||||
|
||||
本 skill 由 Claude **全自动执行**完整的 Flash 烧写流程。除了向用户确认必要信息外,所有操作(文件检查、CSV 修改、Vivado 命令执行、硬件编程)均由 Claude 直接完成,不要求用户手动执行任何命令。
|
||||
本 skill 由 Claude **全自动执行**完整的 Flash 固化流程。除了向用户确认必要信息外,所有操作(文件检查、备份、哈希/容量/内容守卫、工具调用、硬件编程)均由 Claude 直接完成,不要求用户手动执行任何命令。执行前必须先识别开发板与 FPGA/SoC 类型;不得把 Artix-7 的 `.mcs/cfgmem` 流程用于 Zynq,也不得把 Zynq 的 `BOOT.bin/program_flash` 流程用于普通 Artix-7。
|
||||
|
||||
---
|
||||
|
||||
@@ -34,6 +34,9 @@ scripts/
|
||||
program_flash.py # 第四、五步:连接目标板并烧写 Flash
|
||||
step3_write_mcs.tcl # gen_mcs.py 使用的 Tcl 模板(勿手动修改)
|
||||
step4_5_program.tcl # program_flash.py 使用的 Tcl 模板(勿手动修改)
|
||||
build_zynq_boot.py # Zynq:FSBL + bit + PS ELF 生成受保护的 BOOT.bin
|
||||
program_zynq_qspi.py # Zynq:哈希/容量/marker 守卫后烧写并强制 verify
|
||||
vivado_env.py # 解析 Vivado/SDK 工具路径
|
||||
```
|
||||
|
||||
**调用方式(Bash 工具):**
|
||||
@@ -41,10 +44,153 @@ scripts/
|
||||
python "$USERPROFILE/.claude/skills/fpga_flash/scripts/<script>.py" [参数...]
|
||||
```
|
||||
|
||||
各脚本通过命令行参数接受变量(Vivado 安装目录、工程名、bit 文件路径、mcs 文件路径等),内部调用 Vivado `-mode batch -source <tcl模板> -tclargs <参数>` 完成执行,全程不产生临时文件。
|
||||
各脚本通过命令行参数接受变量(Vivado/SDK 安装目录、bit/ELF/MCS/BOOT 路径、容量和 marker 等)。Artix-7 脚本内部调用 Vivado `-mode batch -source <tcl模板> -tclargs <参数>`;Zynq 脚本调用 SDK 的 `bootgen`/`program_flash`。工程路径和板级文件均由参数传入,不在 skill 中硬编码。
|
||||
|
||||
---
|
||||
|
||||
## 第零步:识别板卡并选择唯一流程
|
||||
|
||||
先用工程名、器件型号、原理图/板卡说明和现有部署资料交叉确认,不允许只根据输出文件扩展名猜测。
|
||||
|
||||
| 板卡/器件 | 固化容器 | 写入工具与接口 | 选择的流程 |
|
||||
|---|---|---|---|
|
||||
| HJX9009、`xc7z100`、Zynq-7000(PS+PL) | `BOOT.bin`,顺序为启动 FSBL + PL `.bit` + PS `.elf` | SDK `bootgen` + `program_flash -flash_type qspi_dual_parallel` | 本文“Zynq/HJX9009”流程 |
|
||||
| EMV7.1、`xc7a*`、Artix-7(纯 PL) | `.mcs` | Vivado `write_cfgmem` + Hardware Manager cfgmem | 本文“Artix-7/EMV7.1”流程 |
|
||||
|
||||
硬阻断规则:
|
||||
|
||||
- 发现 `xc7z*`、PS7、FSBL、PS ELF 或工程明确写明 Zynq 时,禁止运行 `gen_mcs.py`、`program_flash.py` 或 `create_hw_cfgmem`。
|
||||
- 发现 `xc7a*` 且无 PS/FSBL 时,禁止生成 `BOOT.bin`。
|
||||
- 板型、Flash 拓扑或容量不明确时,先查原理图/现有已验证部署记录;不得试烧。
|
||||
- HJX9009 已验证为两片 `s25fl128s` 双并行 QSPI,总容量 32 MiB,必须使用 `qspi_dual_parallel`,不能退化为 `qspi_single`。
|
||||
|
||||
---
|
||||
|
||||
## Zynq/HJX9009:同时固化 PS 与 PL
|
||||
|
||||
### Z1. 输入与两个 FSBL 角色
|
||||
|
||||
生成冷启动镜像需要三个输入,BIF 中顺序不可改变:
|
||||
|
||||
```text
|
||||
the_ROM_image:
|
||||
{
|
||||
[bootloader]<boot_fsbl.elf>
|
||||
<implemented_pl.bit>
|
||||
<ps_application.elf>
|
||||
}
|
||||
```
|
||||
|
||||
必须区分两个角色不同的 FSBL:
|
||||
|
||||
- **启动 FSBL**:封装在 `BOOT.bin` 第一分区,板卡冷启动时由 BootROM 加载,随后配置 PL 并启动 PS 应用。
|
||||
- **JTAG 烧写辅助 FSBL**:由 `program_flash -fsbl` 临时下载到 OCM,用于初始化当前板卡的 QSPI MIO/控制器并完成写入。它不等于、也不能默认替换 `BOOT.bin` 内的启动 FSBL。
|
||||
|
||||
对 HJX9009,必须使用已验证能初始化该板 QSPI MIO 的参考 JTAG 辅助 FSBL。若项目提供其期望 SHA-256/MD5,必须作为守卫验证。错误的辅助 FSBL 可能读出 JEDEC `00,00,00`,造成表面执行但没有可靠写入。
|
||||
|
||||
HJX9009 当前工程部署记录中的参考辅助 FSBL 指纹为 MD5 `14232757864453B418E67D63457EE24F`。这只是板级身份参考,不应写死进通用脚本;实际执行仍应从当前项目部署清单取得文件并同时记录 SHA-256。
|
||||
|
||||
### Z2. 安全预检与备份顺序
|
||||
|
||||
执行顺序固定如下:
|
||||
|
||||
1. 确认本次 PS ELF、时序通过的 PL bitstream、启动 FSBL 和 JTAG 辅助 FSBL 的绝对路径。
|
||||
2. 记录四个输入的 SHA-256、字节数和修改时间;若有 `.ltx`,另记录其 SHA-256,确保它与 bitstream 同次构建。
|
||||
3. 在覆盖部署目录中的任何已有 `BOOT.bin`、BIF、FSBL、bit 或 ELF 前,复制到带时间戳的备份目录并写 manifest。不得删除已知可回退镜像。
|
||||
4. 对源文件和 staging 副本逐个比较 SHA-256,任一不一致立即停止。
|
||||
5. 为新固件指定至少一个能区分版本的 ASCII marker,并在 PS ELF 和生成的 `BOOT.bin` 中检查。marker 不匹配立即停止。
|
||||
6. 生成后要求 `BOOT.bin` 严格小于物理 QSPI 容量;HJX9009 上限为 33,554,432 字节。
|
||||
7. 记录最终 `BOOT.bin` 的 SHA-256 和精确长度;烧写命令必须重新校验这两个值,防止旧镜像或 TOCTOU 误烧。
|
||||
|
||||
### Z3. 生成 BOOT.bin
|
||||
|
||||
调用参数化脚本;示例只表示参数结构,实际路径和 marker 必须来自当前项目。
|
||||
Xilinx SDK 安装根目录由脚本自行解析(`SDK_ROOT` / `SDK_PATH`:先查环境变量,再查
|
||||
`~\skills_env.md`),只有需要临时覆盖时才显式传 `--sdk-dir`:
|
||||
|
||||
```powershell
|
||||
python "$env:USERPROFILE\.claude\skills\fpga_flash\scripts\build_zynq_boot.py" `
|
||||
--boot-fsbl "<cold-boot-fsbl.elf>" `
|
||||
--bit-file "<implemented-top.bit>" `
|
||||
--app-elf "<ps-application.elf>" `
|
||||
--boot-bin "<deployment-dir>\BOOT.bin" `
|
||||
--bif-file "<deployment-dir>\boot.bif" `
|
||||
--capacity-bytes 33554432 `
|
||||
--require-marker "<build-identity-marker>"
|
||||
```
|
||||
|
||||
脚本会备份已有 BOOT/BIF、生成候选镜像、执行容量与 marker 守卫、原子替换最终镜像,并输出 JSON manifest。成功标志为:
|
||||
|
||||
```text
|
||||
ZYNQ_BOOTGEN_PASS size=<n> sha256=<64 hex>
|
||||
```
|
||||
|
||||
如果工程采用 staging 目录,复制 FSBL/bit/ELF 后仍要逐项重算并对比 SHA-256。`BOOT.bin` 约 18 MiB 只是 HJX9009 的常见值,不是成功判据。
|
||||
|
||||
### Z4. 启动 Vivado hw_server 并确认 JTAG
|
||||
|
||||
HJX9009 当前机器上必须由 **Vivado** 的 `hw_server` 给 Platform Cable USB II 加载固件;SDK 自带 hw_server 可能无法完成该动作。先启动并等待枚举出 `xc7z100`:
|
||||
|
||||
```powershell
|
||||
& '<Vivado-root>\bin\hw_server.bat' -s tcp::3121
|
||||
```
|
||||
|
||||
保持服务运行,确认 `tcp:127.0.0.1:3121` 可连接且 JTAG 链中只有预期目标。若 3121 被未知进程占用、目标器件不是 `xc7z100`、板卡掉电或线缆未枚举,停止,不得烧写。
|
||||
|
||||
### Z5. 写入 HJX9009 双并行 QSPI 并强制 verify
|
||||
|
||||
从 Z3 的输出/manifest 复制**精确** SHA-256 和长度后执行:
|
||||
|
||||
```powershell
|
||||
python "$env:USERPROFILE\.claude\skills\fpga_flash\scripts\program_zynq_qspi.py" `
|
||||
--image "<deployment-dir>\BOOT.bin" `
|
||||
--jtag-fsbl "<verified-jtag-helper-fsbl.elf>" `
|
||||
--expect-sha256 "<exact-BOOT.bin-sha256>" `
|
||||
--expect-size <exact-byte-length> `
|
||||
--capacity-bytes 33554432 `
|
||||
--flash-type qspi_dual_parallel `
|
||||
--hw-server-url tcp:127.0.0.1:3121 `
|
||||
--expect-jtag-fsbl-sha256 "<exact-helper-FSBL-sha256>" `
|
||||
--require-marker "<build-identity-marker>" `
|
||||
--log-file "<deployment-dir>\program_flash.log"
|
||||
```
|
||||
|
||||
安全约束:
|
||||
|
||||
- offset 必须为 `0`;HJX9009 的 Flash 类型必须保持 `qspi_dual_parallel`。
|
||||
- 参数顺序必须是 `-verify` 在 `-fsbl` 前,`-cable ...` 最后;参数化脚本已固定此顺序。
|
||||
- 擦除、编程、回读校验可能超过 10 分钟;期间禁止断电、拔 JTAG、关闭 hw_server 或终止进程。
|
||||
- 不信任包装层退出码。已验证存在“无法连接 hw_server 但返回码仍为 0”的情况。
|
||||
|
||||
真正成功必须在同一次日志中同时出现:
|
||||
|
||||
```text
|
||||
Erase Operation successful
|
||||
Program Operation successful
|
||||
Verify Operation successful
|
||||
Flash Operation Successful
|
||||
ZYNQ_QSPI_FLASH_VERIFY_PASS
|
||||
```
|
||||
|
||||
若出现 `Verify Operation unsuccessful`、`Flash Operation Failed`,或缺少任一成功行,一律判失败。`End of startup status: HIGH` 只证明易失配置完成,不能证明 Flash 已写入。
|
||||
|
||||
### Z6. 完全断电冷启动与 COM5 验收
|
||||
|
||||
烧写成功后还必须验证自主启动:
|
||||
|
||||
1. 保存烧写日志、BOOT manifest、bit/LTX/ELF 哈希和回退镜像位置。
|
||||
2. 关闭板卡主电源及可能通过 JTAG 反向供电的接口,静置至少 30 秒;JTAG 枚举异常时按板级规程全断 90 秒。
|
||||
3. 保持 JTAG 断开,重新打开主电源,让板卡只从 QSPI 启动。
|
||||
4. 打开 COM5(115200、8N1、无流控),保存带时间戳的启动日志。
|
||||
5. 要求看到本次 PS 固件的唯一 marker/banner,并执行项目定义的只读状态命令。HJX9009 当前固件通常用 `HELP`、`?`,随后按项目规程输入 `R`;验收期望状态和命令必须以本次固件文档为准。
|
||||
6. 若 marker 不匹配、COM5 无输出、PL/PS 版本不一致或只能在重新接入 JTAG 后工作,判定冷启动失败并使用已备份的已知良好镜像回退。
|
||||
|
||||
只有“写入日志完整成功 + verify 成功 + 完全断电后 COM5 确认本次固件自主启动”三者同时成立,才可以向用户报告 HJX9009 PS+PL 已固化。
|
||||
|
||||
---
|
||||
|
||||
## Artix-7/EMV7.1:MCS/cfgmem 流程(保留)
|
||||
|
||||
## 第一步:检查并修复 Vivado Flash 器件表
|
||||
|
||||
**Claude 自动执行:**
|
||||
@@ -156,3 +302,6 @@ ILA core 的 `Dropping logic core` WARNING 是正常现象(载入 loader/golde
|
||||
| DONE 灯不亮 | 启动模式跳线错误 | 检查 MODE 引脚电平,确认为 SPI 启动模式 |
|
||||
| 烧写后板卡跑旧程序 | Flash 未完全擦除 | 确认 `PROGRAM.ERASE 1` 已设置后重新执行 |
|
||||
| `PermissionError` 写 CSV | 权限不足 | 提示用户以管理员身份运行 Claude Code |
|
||||
| Zynq `program_flash` 返回 0 但无成功行 | hw_server 未连接或工具包装层假成功 | 按日志判失败;要求 erase/program/verify/Flash 四组成功标志齐全 |
|
||||
| HJX9009 JEDEC 为 `00,00,00` | JTAG 辅助 FSBL 未正确初始化 QSPI MIO | 使用项目已验证的参考辅助 FSBL并核对哈希,保持 `qspi_dual_parallel` |
|
||||
| `BOOT.bin` 可写但冷启动无 COM5 | 启动 FSBL/PS ELF/PL 不匹配或未自主启动 | 完全断电、断开 JTAG,从 QSPI 冷启动并核对固件 marker;失败则回退备份 |
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a guarded Zynq-7000 BOOT.bin from FSBL + PL bit + PS ELF.
|
||||
|
||||
The output is generated as a candidate, checked for capacity and required
|
||||
ASCII build markers, then atomically promoted. Existing BOOT.bin/BIF files are
|
||||
backed up before replacement. This script does not program hardware.
|
||||
"""
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from vivado_env import resolve_sdk_dir
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest().upper()
|
||||
|
||||
|
||||
def require_file(label: str, value: str) -> Path:
|
||||
path = Path(value).expanduser().resolve()
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"ERROR: missing {label}: {path}")
|
||||
return path
|
||||
|
||||
|
||||
def bif_path(path: Path) -> str:
|
||||
return path.as_posix()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build guarded Zynq BOOT.bin (boot FSBL + PL bit + PS ELF)"
|
||||
)
|
||||
parser.add_argument("--sdk-dir", help="Xilinx SDK root containing bin/bootgen.bat")
|
||||
parser.add_argument("--boot-fsbl", required=True, help="FSBL executed at cold boot")
|
||||
parser.add_argument("--bit-file", required=True, help="implemented PL .bit file")
|
||||
parser.add_argument("--app-elf", required=True, help="PS application ELF")
|
||||
parser.add_argument("--boot-bin", required=True, help="final BOOT.bin path")
|
||||
parser.add_argument("--bif-file", help="persistent BIF path; default beside BOOT.bin")
|
||||
parser.add_argument("--capacity-bytes", type=int, default=32 * 1024 * 1024)
|
||||
parser.add_argument(
|
||||
"--require-marker", action="append", default=[],
|
||||
help="ASCII marker that must exist in the generated image; repeatable",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--backup-dir",
|
||||
help="backup root; default <BOOT.bin directory>/flash_backups",
|
||||
)
|
||||
parser.add_argument("--dry-run", action="store_true", help="validate and print only")
|
||||
args = parser.parse_args()
|
||||
|
||||
boot_fsbl = require_file("boot FSBL", args.boot_fsbl)
|
||||
bit_file = require_file("PL bitstream", args.bit_file)
|
||||
app_elf = require_file("PS application ELF", args.app_elf)
|
||||
boot_bin = Path(args.boot_bin).expanduser().resolve()
|
||||
bif_file = (
|
||||
Path(args.bif_file).expanduser().resolve()
|
||||
if args.bif_file else boot_bin.with_suffix(".bif")
|
||||
)
|
||||
if args.capacity_bytes <= 0:
|
||||
raise SystemExit("ERROR: --capacity-bytes must be positive")
|
||||
try:
|
||||
marker_bytes = [(marker, marker.encode("ascii")) for marker in args.require_marker]
|
||||
except UnicodeEncodeError:
|
||||
raise SystemExit("ERROR: --require-marker values must be ASCII")
|
||||
app_bytes = app_elf.read_bytes()
|
||||
missing_app_markers = [marker for marker, raw in marker_bytes if raw not in app_bytes]
|
||||
if missing_app_markers:
|
||||
raise SystemExit(
|
||||
"ERROR: PS application ELF lacks required markers: " + repr(missing_app_markers)
|
||||
)
|
||||
|
||||
sdk_dir = Path(resolve_sdk_dir(args.sdk_dir)).expanduser().resolve()
|
||||
bootgen = sdk_dir / "bin" / "bootgen.bat"
|
||||
if not bootgen.is_file():
|
||||
raise SystemExit(f"ERROR: bootgen not found: {bootgen}")
|
||||
|
||||
bif_text = (
|
||||
"the_ROM_image:\n"
|
||||
"{\n"
|
||||
f" [bootloader]{bif_path(boot_fsbl)}\n"
|
||||
f" {bif_path(bit_file)}\n"
|
||||
f" {bif_path(app_elf)}\n"
|
||||
"}\n"
|
||||
)
|
||||
candidate = boot_bin.with_name(f"{boot_bin.name}.candidate.{os.getpid()}")
|
||||
cmd = [
|
||||
str(bootgen), "-image", str(bif_file), "-arch", "zynq",
|
||||
"-o", str(candidate), "-w", "on",
|
||||
]
|
||||
print(f"[build_zynq_boot] boot FSBL: {boot_fsbl} sha256={sha256(boot_fsbl)}")
|
||||
print(f"[build_zynq_boot] PL bit : {bit_file} sha256={sha256(bit_file)}")
|
||||
print(f"[build_zynq_boot] PS ELF : {app_elf} sha256={sha256(app_elf)}")
|
||||
print(f"[build_zynq_boot] output : {boot_bin}")
|
||||
print(f"[build_zynq_boot] capacity : {args.capacity_bytes}")
|
||||
print("[build_zynq_boot] command : " + subprocess.list2cmdline(cmd))
|
||||
if args.dry_run:
|
||||
print("DRY_RUN_OK: no files changed and bootgen was not executed")
|
||||
return
|
||||
|
||||
boot_bin.parent.mkdir(parents=True, exist_ok=True)
|
||||
bif_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
stamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_root = (
|
||||
Path(args.backup_dir).expanduser().resolve()
|
||||
if args.backup_dir else boot_bin.parent / "flash_backups"
|
||||
)
|
||||
existing = [path for path in (boot_bin, bif_file) if path.exists()]
|
||||
if existing:
|
||||
backup = backup_root / stamp
|
||||
backup.mkdir(parents=True, exist_ok=False)
|
||||
for path in existing:
|
||||
shutil.copy2(path, backup / path.name)
|
||||
print(f"[build_zynq_boot] backup : {backup}")
|
||||
|
||||
bif_file.write_text(bif_text, encoding="utf-8", newline="\n")
|
||||
try:
|
||||
result = subprocess.run(cmd, check=False)
|
||||
if result.returncode != 0 or not candidate.is_file():
|
||||
raise RuntimeError(f"bootgen failed, rc={result.returncode}")
|
||||
size = candidate.stat().st_size
|
||||
if size >= args.capacity_bytes:
|
||||
raise RuntimeError(
|
||||
f"candidate size {size} is not below capacity {args.capacity_bytes}"
|
||||
)
|
||||
image = candidate.read_bytes()
|
||||
missing = [marker for marker, raw in marker_bytes if raw not in image]
|
||||
if missing:
|
||||
raise RuntimeError("candidate lacks required markers: " + repr(missing))
|
||||
candidate_hash = sha256(candidate)
|
||||
os.replace(candidate, boot_bin)
|
||||
except Exception as exc:
|
||||
if candidate.exists():
|
||||
candidate.unlink()
|
||||
raise SystemExit(f"ERROR: {exc}")
|
||||
|
||||
manifest = {
|
||||
"format": "zynq-7000-boot-bin",
|
||||
"created": dt.datetime.now().astimezone().isoformat(),
|
||||
"capacity_bytes": args.capacity_bytes,
|
||||
"required_markers": args.require_marker,
|
||||
"components": {
|
||||
"boot_fsbl": {"path": str(boot_fsbl), "sha256": sha256(boot_fsbl)},
|
||||
"pl_bit": {"path": str(bit_file), "sha256": sha256(bit_file)},
|
||||
"ps_elf": {"path": str(app_elf), "sha256": sha256(app_elf)},
|
||||
},
|
||||
"boot_bin": {"path": str(boot_bin), "size": size, "sha256": candidate_hash},
|
||||
}
|
||||
manifest_path = boot_bin.with_suffix(boot_bin.suffix + ".manifest.json")
|
||||
manifest_path.write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
print(f"ZYNQ_BOOTGEN_PASS size={size} sha256={candidate_hash}")
|
||||
print(f"MANIFEST={manifest_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guard and program a Zynq BOOT.bin through Xilinx program_flash.
|
||||
|
||||
Success requires erase, program, verify, and the final Xilinx success marker;
|
||||
the process exit code alone is deliberately not trusted.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from vivado_env import resolve_sdk_dir
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest().upper()
|
||||
|
||||
|
||||
def parse_server(url: str):
|
||||
match = re.fullmatch(r"tcp:([^:]+):(\d+)", url)
|
||||
if not match:
|
||||
raise SystemExit("ERROR: --hw-server-url must look like tcp:127.0.0.1:3121")
|
||||
return match.group(1), int(match.group(2))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Program and verify Zynq QSPI BOOT.bin")
|
||||
parser.add_argument("--sdk-dir", help="Xilinx SDK root containing bin/program_flash.bat")
|
||||
parser.add_argument("--image", required=True, help="BOOT.bin to program at offset 0")
|
||||
parser.add_argument("--jtag-fsbl", required=True, help="helper FSBL loaded to OCM for programming")
|
||||
parser.add_argument("--expect-sha256", required=True, help="exact expected BOOT.bin SHA-256")
|
||||
parser.add_argument("--expect-size", type=int, help="exact expected BOOT.bin byte length")
|
||||
parser.add_argument("--capacity-bytes", type=int, default=32 * 1024 * 1024)
|
||||
parser.add_argument("--expect-jtag-fsbl-sha256", help="optional exact helper FSBL SHA-256")
|
||||
parser.add_argument(
|
||||
"--require-marker", action="append", default=[],
|
||||
help="ASCII marker required in BOOT.bin; repeatable",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--flash-type",
|
||||
choices=["qspi_single", "qspi_x1_single", "qspi_x2_single",
|
||||
"qspi_dual_parallel", "qspi_dual_stacked",
|
||||
"qspi_x1_dual_stacked", "qspi_x2_dual_stacked"],
|
||||
default="qspi_dual_parallel",
|
||||
)
|
||||
parser.add_argument("--offset", default="0")
|
||||
parser.add_argument("--hw-server-url", default="tcp:127.0.0.1:3121")
|
||||
parser.add_argument("--log-file", help="write complete program_flash output here")
|
||||
parser.add_argument("--dry-run", action="store_true", help="run guards and print command only")
|
||||
args = parser.parse_args()
|
||||
|
||||
image = Path(args.image).expanduser().resolve()
|
||||
jtag_fsbl = Path(args.jtag_fsbl).expanduser().resolve()
|
||||
for label, path in (("BOOT.bin", image), ("JTAG helper FSBL", jtag_fsbl)):
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"ERROR: missing {label}: {path}")
|
||||
if args.capacity_bytes <= 0:
|
||||
raise SystemExit("ERROR: --capacity-bytes must be positive")
|
||||
if not re.fullmatch(r"[0-9A-Fa-f]{64}", args.expect_sha256):
|
||||
raise SystemExit("ERROR: --expect-sha256 must contain exactly 64 hex digits")
|
||||
if (args.expect_jtag_fsbl_sha256 and
|
||||
not re.fullmatch(r"[0-9A-Fa-f]{64}", args.expect_jtag_fsbl_sha256)):
|
||||
raise SystemExit(
|
||||
"ERROR: --expect-jtag-fsbl-sha256 must contain exactly 64 hex digits"
|
||||
)
|
||||
try:
|
||||
if int(args.offset, 0) != 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise SystemExit("ERROR: Zynq BOOT.bin must be programmed at offset 0")
|
||||
|
||||
image_size = image.stat().st_size
|
||||
image_hash = sha256(image)
|
||||
expected_hash = args.expect_sha256.upper()
|
||||
if image_hash != expected_hash:
|
||||
raise SystemExit(f"ERROR: BOOT.bin SHA256 {image_hash} != expected {expected_hash}")
|
||||
if args.expect_size is not None and image_size != args.expect_size:
|
||||
raise SystemExit(
|
||||
f"ERROR: BOOT.bin size {image_size} != expected {args.expect_size}"
|
||||
)
|
||||
if image_size >= args.capacity_bytes:
|
||||
raise SystemExit(
|
||||
f"ERROR: BOOT.bin size {image_size} is not below capacity {args.capacity_bytes}"
|
||||
)
|
||||
try:
|
||||
marker_bytes = [(marker, marker.encode("ascii")) for marker in args.require_marker]
|
||||
except UnicodeEncodeError:
|
||||
raise SystemExit("ERROR: --require-marker values must be ASCII")
|
||||
image_bytes = image.read_bytes()
|
||||
missing = [marker for marker, raw in marker_bytes if raw not in image_bytes]
|
||||
if missing:
|
||||
raise SystemExit("ERROR: BOOT.bin lacks required markers: " + repr(missing))
|
||||
|
||||
helper_hash = sha256(jtag_fsbl)
|
||||
if (args.expect_jtag_fsbl_sha256 and
|
||||
helper_hash != args.expect_jtag_fsbl_sha256.upper()):
|
||||
raise SystemExit(
|
||||
f"ERROR: JTAG helper FSBL SHA256 {helper_hash} != expected "
|
||||
f"{args.expect_jtag_fsbl_sha256.upper()}"
|
||||
)
|
||||
|
||||
sdk_dir = Path(resolve_sdk_dir(args.sdk_dir)).expanduser().resolve()
|
||||
program_flash = sdk_dir / "bin" / "program_flash.bat"
|
||||
if not program_flash.is_file():
|
||||
raise SystemExit(f"ERROR: program_flash not found: {program_flash}")
|
||||
cmd = [
|
||||
str(program_flash), "-f", str(image), "-offset", args.offset,
|
||||
"-flash_type", args.flash_type, "-verify", "-fsbl", str(jtag_fsbl),
|
||||
"-cable", "type", "xilinx_tcf", "url", args.hw_server_url,
|
||||
]
|
||||
print(f"[program_zynq_qspi] image : {image}")
|
||||
print(f"[program_zynq_qspi] size/sha256 : {image_size} / {image_hash}")
|
||||
print(f"[program_zynq_qspi] helper FSBL : {jtag_fsbl} / {helper_hash}")
|
||||
print(f"[program_zynq_qspi] flash/server: {args.flash_type} / {args.hw_server_url}")
|
||||
print("[program_zynq_qspi] command : " + subprocess.list2cmdline(cmd))
|
||||
if args.dry_run:
|
||||
print("DRY_RUN_OK: guards passed; hardware was not touched")
|
||||
return
|
||||
|
||||
host, port = parse_server(args.hw_server_url)
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=3):
|
||||
pass
|
||||
except OSError as exc:
|
||||
raise SystemExit(f"ERROR: hw_server is not reachable at {host}:{port}: {exc}")
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, errors="replace")
|
||||
combined = (result.stdout or "") + (result.stderr or "")
|
||||
sys.stdout.write(combined)
|
||||
print(f"PROGRAM_FLASH_EXIT={result.returncode}")
|
||||
if args.log_file:
|
||||
log_path = Path(args.log_file).expanduser().resolve()
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_path.write_text(combined, encoding="utf-8", errors="replace")
|
||||
print(f"PROGRAM_FLASH_LOG={log_path}")
|
||||
|
||||
required = [
|
||||
"Erase Operation successful",
|
||||
"Program Operation successful",
|
||||
"Verify Operation successful",
|
||||
"Flash Operation Successful",
|
||||
]
|
||||
missing_success = [marker for marker in required if marker not in combined]
|
||||
explicit_failure = re.search(
|
||||
r"(Verify Operation unsuccessful|Flash Operation Failed|Program Operation unsuccessful)",
|
||||
combined, re.IGNORECASE,
|
||||
)
|
||||
if result.returncode != 0 or missing_success or explicit_failure:
|
||||
raise SystemExit(
|
||||
"ERROR: flash not proven successful; rc={} missing={} explicit_failure={}".format(
|
||||
result.returncode, missing_success, bool(explicit_failure)
|
||||
)
|
||||
)
|
||||
print("ZYNQ_QSPI_FLASH_VERIFY_PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -56,3 +56,23 @@ def resolve_vivado_exe(cli_value=None):
|
||||
if exe:
|
||||
return exe
|
||||
return os.path.join(resolve_vivado_dir(), "bin", "vivado.bat")
|
||||
|
||||
|
||||
def resolve_sdk_dir(cli_value=None):
|
||||
"""Xilinx SDK install ROOT (the dir containing bin/bootgen.bat).
|
||||
|
||||
Precedence matches the Vivado resolver: explicit CLI value, SDK_ROOT,
|
||||
SDK_PATH, then the same keys in ~/skills_env.md.
|
||||
"""
|
||||
if cli_value:
|
||||
return cli_value
|
||||
root = env_lookup("SDK_ROOT")
|
||||
if root:
|
||||
return root
|
||||
exe = env_lookup("SDK_PATH")
|
||||
if exe:
|
||||
return os.path.dirname(os.path.dirname(exe))
|
||||
sys.exit(
|
||||
"Xilinx SDK path not configured. Add `SDK_ROOT = <install root>` to "
|
||||
"~/skills_env.md, set it as an env var, or pass --sdk-dir."
|
||||
)
|
||||
|
||||
@@ -30,6 +30,25 @@ import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
def _env_lookup(key, default=None):
|
||||
"""Machine-relative value: real env var first, then ~/skills_env.md."""
|
||||
v = os.environ.get(key)
|
||||
if v:
|
||||
return v
|
||||
path = os.path.join(os.path.expanduser("~"), "skills_env.md")
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
m = re.match(rf"\s*{key}\s*=\s*(\S+)", line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
except OSError:
|
||||
pass
|
||||
if default is None:
|
||||
sys.exit(f"missing {key}: add it to ~/skills_env.md or set it as an env var")
|
||||
return default
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ protocol constants
|
||||
|
||||
HEAD, TAIL = 0xAA, 0xBB
|
||||
@@ -246,7 +265,7 @@ def do_finish(dev, timeout):
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--mcs", required=True, help="path to the quickboot <tag>_update.mcs")
|
||||
ap.add_argument("--host", default="192.168.1.143")
|
||||
ap.add_argument("--host", default=_env_lookup("SMOD_BOARD_IP", "192.168.1.143"))
|
||||
ap.add_argument("--port", type=int, default=1234)
|
||||
ap.add_argument("--yes", action="store_true", help="skip the confirmation prompt")
|
||||
ap.add_argument("--dry-run", action="store_true", help="validate and frame the mcs, contact nothing")
|
||||
|
||||
@@ -26,16 +26,6 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
REPO = r"E:\Xilinx_design\smod\SMOD_ATSC\my_exciter"
|
||||
TCL_DIR = os.path.join(REPO, "my_exciter.srcs", "tcl_1")
|
||||
BITSTREAM = os.path.join(REPO, "bitstream")
|
||||
IMPL = os.path.join(REPO, "my_exciter.runs", "impl_1")
|
||||
|
||||
POST_BITSTREAM = os.path.join(TCL_DIR, "post_bitstream.tcl")
|
||||
GEN_RU = os.path.join(TCL_DIR, "runScript_GenRu.tcl")
|
||||
PERL_SCRIPT = os.path.join(TCL_DIR, "MakeSpiFlashProgrammerMcsFiles_backup.pl")
|
||||
|
||||
|
||||
def _env_lookup(key, default=None):
|
||||
"""Machine-local values (paths, IPs) live in ~/skills_env.md, never in git.
|
||||
An OS environment variable of the same name overrides the file."""
|
||||
@@ -56,6 +46,18 @@ def _env_lookup(key, default=None):
|
||||
return default
|
||||
|
||||
|
||||
# The my_exciter checkout root differs per machine; default keeps this machine's
|
||||
# historical path so behaviour is unchanged when the key is absent.
|
||||
REPO = _env_lookup("SMOD_REPO_ROOT", r"E:\Xilinx_design\smod\SMOD_ATSC\my_exciter")
|
||||
TCL_DIR = os.path.join(REPO, "my_exciter.srcs", "tcl_1")
|
||||
BITSTREAM = os.path.join(REPO, "bitstream")
|
||||
IMPL = os.path.join(REPO, "my_exciter.runs", "impl_1")
|
||||
|
||||
POST_BITSTREAM = os.path.join(TCL_DIR, "post_bitstream.tcl")
|
||||
GEN_RU = os.path.join(TCL_DIR, "runScript_GenRu.tcl")
|
||||
PERL_SCRIPT = os.path.join(TCL_DIR, "MakeSpiFlashProgrammerMcsFiles_backup.pl")
|
||||
|
||||
|
||||
def resolve_vivado():
|
||||
"""Vivado's install path differs per machine -- never hardcode it."""
|
||||
exe = _env_lookup("VIVADO_PATH")
|
||||
|
||||
@@ -32,22 +32,58 @@ foreach ip $ips {
|
||||
}
|
||||
switch -- $c(type) {
|
||||
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 \
|
||||
-module_name $name -dir $ipdir
|
||||
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]
|
||||
if {$dmode eq "dual_port_ram"} {
|
||||
set_property -dict [list \
|
||||
CONFIG.memory_type {dual_port_ram} \
|
||||
CONFIG.data_width $c(width) \
|
||||
CONFIG.depth $c(depth) \
|
||||
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 {
|
||||
# SDP, common clock, READ_FIRST on the write port, no output register,
|
||||
# ENA/ENB pins -- matches SimpleDualPortRAM_generic semantics exactly.
|
||||
# mode {sdp_readfirst_commonclk} (default): SDP, common clock,
|
||||
# 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 \
|
||||
-module_name $name -dir $ipdir
|
||||
set_property -dict [list \
|
||||
CONFIG.Memory_Type {Simple_Dual_Port_RAM} \
|
||||
CONFIG.Memory_Type $mtype \
|
||||
CONFIG.Assume_Synchronous_Clk {true} \
|
||||
CONFIG.Use_Byte_Write_Enable {false} \
|
||||
CONFIG.Write_Width_A $c(width) \
|
||||
@@ -55,12 +91,19 @@ foreach ip $ips {
|
||||
CONFIG.Read_Width_A $c(width) \
|
||||
CONFIG.Write_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_B {Use_ENB_Pin} \
|
||||
CONFIG.Register_PortB_Output_of_Memory_Primitives {false} \
|
||||
CONFIG.Register_PortB_Output_of_Memory_Core {false} \
|
||||
] [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 {
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
@@ -19,13 +39,20 @@ def main():
|
||||
d = os.path.abspath(a.rtl_ip_dir)
|
||||
files = [os.path.join(d, f) for f in os.listdir(d) if f.endswith('.v')]
|
||||
|
||||
mods = []
|
||||
mods, already = [], []
|
||||
for p in files:
|
||||
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'):
|
||||
mods.append(m.group(1))
|
||||
n = 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)
|
||||
print('modules:', mods)
|
||||
if already:
|
||||
print('already namespaced (kept in the rename set as base names):',
|
||||
sorted(set(already)))
|
||||
|
||||
for p in files:
|
||||
txt = open(p, encoding='utf-8', errors='replace').read()
|
||||
|
||||
+182
-21
@@ -2,14 +2,19 @@
|
||||
name: sim-rtl-flow
|
||||
description: >
|
||||
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) →
|
||||
HUMAN sign-off on simulation performance → optional -q fixed-point → parallel
|
||||
functional partition + RTL/SoC interface spec → HUMAN sign-off on the partition →
|
||||
per-module -E.slx nativization and Xilinx-IP replacement (parallel Send fanout) →
|
||||
Vivado build + timing → hardware-in-the-loop via fpga-imx6-flow or zynq-imx6-flow.
|
||||
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.
|
||||
Simulink-to-RTL-to-hardware design pipeline: read the previous run's design version-tree
|
||||
doc xxx_tree.md to decide what may be reused → Simulink -x principle sim
|
||||
(sim-golden) → HUMAN sign-off on simulation performance → optional -q fixed-point →
|
||||
parallel functional partition + RTL/SoC interface spec → HUMAN sign-off on the
|
||||
partition (skippable when history says the partition is unchanged) → per-module
|
||||
-E.slx nativization and Xilinx-IP replacement (parallel Send fanout, modules the
|
||||
history proved good are skipped) → constraint-primitive injection into the delivered
|
||||
RTL tree → Vivado build + timing → hardware-in-the-loop via
|
||||
fpga-imx6-flow or zynq-imx6-flow → write a fresh xxx<date>_tree.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
|
||||
the sim-rtl-flow workflow / Simulink→RTL design flow, or says things like "启动
|
||||
@@ -22,8 +27,9 @@ description: >
|
||||
|
||||
## What this skill does
|
||||
|
||||
Drives the **sim-rtl-flow** graph (21 top-level nodes + a 3-node per-module subgraph,
|
||||
1:1 with `Notes/PR01-1.md`, `Notes/PR01-2.md` and `Notes/PR01_sim_rtl_flow.drawio`).
|
||||
Drives the **sim-rtl-flow** graph (27 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`,
|
||||
plus the constraint-primitive stage from `Notes/PR23_flow_constraint_stage.md`).
|
||||
|
||||
The graph contains **no** agent logic — it talks to the outside world through two
|
||||
protocols: `SubagentBackend` (design work) and `ReviewGate` (the two mandatory human
|
||||
@@ -60,6 +66,51 @@ export PYTHONIOENCODING=utf-8
|
||||
The driver resolves the workflow checkout from `SIM_RTL_FLOW_ROOT` in
|
||||
`~\skills_env.md`, so no path is baked into this skill.
|
||||
|
||||
## 📄 The design version-tree doc `xxx<date>_tree.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 | `<project_name><YYYYMMDD-HH>_tree.md` — project name + date **and hour**, zero-padded, so name order == time order. A hand-written `xxx_tree.md` with no stamp counts as the oldest (it is the human-supplied starting point). Pre-rename `sim-rtl-flow-<date>.md` files are still read when no `*_tree.md` exists |
|
||||
| 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. Before answering `false`, re-check the historic `-q` against the **current** word-length rule — widths that violate it, or a history doc that never recorded widths, mean `true` |
|
||||
| `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` |
|
||||
| `redo_constraints` + `constraint_spec_hash` | `redo_constraints: false` → the `constraint_primitives` stage is skipped, **but only if no module regenerated RTL this round** (graph-enforced). Answer `false` only after actually hashing the current spec file and matching it against the hash in the history doc; no hash recorded = `true` |
|
||||
|
||||
**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 graph stops at two points that **an agent may not decide**:
|
||||
@@ -67,7 +118,7 @@ The graph stops at two points that **an agent may not decide**:
|
||||
| checkpoint | where | what the user confirms |
|
||||
|---|---|---|
|
||||
| `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
|
||||
to the user verbatim, then wait.** Do not approve on your own judgement, do not infer
|
||||
@@ -90,25 +141,84 @@ From `Notes/PR01.md` and `Notes/PR01-2.md`; the graph routes on these.
|
||||
1. **sim-golden is the only acceptance criterion.** `-q`, the partition, and every `-E`
|
||||
version must match the `-x` sim. If it doesn't, record `gate_passed: false` — never
|
||||
record a pass you didn't verify.
|
||||
2. **One `-E.slx` per RTL submodule**, saved separately (`<project>_A-E.slx`, …). Do not
|
||||
2. **The fixed-point word-length rule ranks alongside sim-golden.** Only multiplies and
|
||||
adds of 16bit or less — after a 16×16 product, truncate back to 16bit before anything
|
||||
downstream; widths must not accumulate stage by stage. The one exception is loop
|
||||
accumulation: the accumulator is at least 32bit, and its result is truncated before it
|
||||
feeds the rest of the design. `fixed_point#n` fails on a violation even when the
|
||||
performance matches sim-golden, and `native:*`, `modify_e:*`, `sync_sim:*` may not
|
||||
widen anything back. The rule and its machine-readable form arrive in every affected
|
||||
task as `context.wordlength_rule` / `context.wordlength_limits`.
|
||||
3. **One `-E.slx` per RTL submodule**, saved separately (`<project>_A-E.slx`, …). Do not
|
||||
nativize all submodules in one Simulink model.
|
||||
3. **Never hand-edit MATLAB-generated RTL.** If IP replacement, Vivado timing, or HIL
|
||||
4. **Never hand-edit MATLAB-generated RTL.** If IP replacement, Vivado timing, or HIL
|
||||
needs a change there, return `change_scope: "simulink_module"` + `affected_modules`.
|
||||
The graph then forces `modify_e:* → sync_sim:*` before any RTL is regenerated.
|
||||
4. **Editing a `-E` model obliges you to sync the `-x`/`-q` model.** `modify_e:*` edits
|
||||
**The one sanctioned way to add something HDL Coder cannot express is
|
||||
`constraint_primitives` — and it is not an exception to this rule.** See below.
|
||||
5. **Editing a `-E` model obliges you to sync the `-x`/`-q` model.** `modify_e:*` edits
|
||||
only the `-E`; the follow-up `sync_sim:*` uses **`simulink-rtl-redev`** to carry the
|
||||
same change into the simulation model and re-verify against sim-golden. The
|
||||
derivation script does *not* translate native-core edits into the m implementation,
|
||||
so changing one side alone means the RTL moved while the simulation still validates
|
||||
the old behaviour. If they don't align, the graph goes back to `modify_e:*` — it will
|
||||
**not** let you regenerate RTL.
|
||||
5. **Timing fixes change the RTL, not the constraints.** Prefer adding pipeline/delay
|
||||
6. **Timing fixes change the RTL, not the constraints.** Prefer adding pipeline/delay
|
||||
stages. Only touch timing constraints when the constraint itself is plainly wrong,
|
||||
and say why in `notes`.
|
||||
6. **HIL is strictly read-only.** `hil:*` arrives with `allow_code_changes=false`. Run
|
||||
7. **HIL is strictly read-only.** `hil:*` arrives with `allow_code_changes=false`. Run
|
||||
`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
|
||||
sim-rtl-flow main graph, then re-enters Vivado and HIL.
|
||||
8. **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 `xxx<date>_tree.md`; anything you
|
||||
could not verify goes in as *not verified*, so the next run redoes it.
|
||||
9. **Synthesis attributes are injected, never typed in.** See the next section.
|
||||
|
||||
## 🔧 步骤 4-3 · 约束原语 `constraint_primitives`
|
||||
|
||||
Between `ip_replace:*` and `vivado`, one **global** node injects the synthesis
|
||||
attributes HDL Coder cannot express — `DONT_TOUCH`, `shreg_extract="no"`, `KEEP`
|
||||
(ILA instantiation later) — into the **delivered RTL tree**.
|
||||
|
||||
**Why it exists.** Synthesis merges bit-identical registers. A 32-bit pipeline
|
||||
(`gk_tp_ra`) was folded away with `[Synth 8-4471] merging register … into
|
||||
gk_RUN_out1_reg[31:0]`: the net name survives, the flip-flops are gone, and **the
|
||||
timing report shows nothing at all** — it was caught only by the area accounting.
|
||||
An earlier instance of the same merge cost 178 failing endpoints. HDL Coder has no
|
||||
way to express an anti-merge attribute, so without this stage the design silently
|
||||
ships without them, every single regeneration.
|
||||
|
||||
**Why it is not "hand-editing generated RTL".** The rule exists to stop the RTL and
|
||||
the model from **silently diverging**. This stage is driven by a *declarative spec*,
|
||||
is *idempotent*, and is *re-runnable*: regenerate the RTL, run it again, and the
|
||||
result is byte-for-byte the same. A replayable injector produces no divergence, so
|
||||
it is not a hand edit — and it is the **only** sanctioned route for these attributes.
|
||||
Hand-typing one into a generated file is still forbidden.
|
||||
|
||||
**Where it sits, and why globally rather than per module.** The spec is one file for
|
||||
the whole design and its attributes may target cross-module or top-level signals; the
|
||||
injector operates on the whole `ip_replace`-output tree. A global node also covers
|
||||
modules reused from history — those skip `native`/`ip_replace` but their RTL is still
|
||||
in the delivered tree and still needs the attributes.
|
||||
|
||||
**Invariant: every path that changed the RTL tree re-injects before Vivado.**
|
||||
`main_timing_rework` and `main_hil_rework` therefore return to
|
||||
`constraint_primitives`, not straight to `vivado`. A plain `vivado` retry does not
|
||||
re-inject (it changed nothing).
|
||||
|
||||
| failure shape | route |
|
||||
|---|---|
|
||||
| `gate_passed: false` | **retry `constraint_primitives`** — an injection gate failure is normally a spec/script problem, not a model problem. Never goes to `modify_e` on its own |
|
||||
| `metadata.idempotent_verified: false` | fails the gate **on its own**, even with `gate_passed: true` — then retries |
|
||||
| `needs_code_change: true` + `change_scope: "simulink_module"` + `affected_modules` | `modify_e:constraints` → `sync_sim:constraints` → only those modules re-run `native → ip_replace` → re-inject. Use this **only** when the spec proves the signal's shape in the `-E` model is wrong (target absent, structurally merged, width/hierarchy mismatch) |
|
||||
|
||||
**Reuse.** Skipped only when all three hold: a history doc exists, `history_review`
|
||||
returned `redo_constraints: false` (it must have hashed the current spec and compared
|
||||
it to the hash in the history doc), **and no module regenerated RTL this round**. The
|
||||
last condition is enforced by the graph — a subagent cannot override it, because a
|
||||
regenerated tree has lost last round's attributes. Default, as everywhere here, is redo.
|
||||
|
||||
## Run (agent path)
|
||||
|
||||
@@ -140,6 +250,9 @@ EOF
|
||||
| `fixed_point_required` | `false` skips the `-q` stage |
|
||||
| `rtl_modules` | submodule names; each gets its own parallel `native → ip_replace` chain. Non-empty. |
|
||||
| `soc_modules` | MCU/SoC-side modules (interface-spec input) |
|
||||
| `constraint_spec_path` | optional — the constraint-primitive spec file, relative to `work_dir`. A hint only; the subagent's reported `metadata.spec_path` is authoritative |
|
||||
| `history_dir` | optional — folder holding `xxx<date>_tree.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
|
||||
|
||||
@@ -182,21 +295,24 @@ keys with a default success — rehearsal only, and it refuses review keys.
|
||||
|
||||
| key | what you actually do | which skill to use |
|
||||
|---|---|---|
|
||||
| `history_review#n` | read `xxx<date>_tree.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` |
|
||||
| `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`, **and hold the word-length rule** (mult/add ≤16bit with truncation after the product; loop accumulators ≥32bit then truncated). Report `metadata.{wordlength_compliant, wordlength_violations, wordlengths}` — a violation fails the gate even when the performance matches sim-golden | same MATLAB skills |
|
||||
| `partition#n` | pure-sim / RTL / SoC split, must not degrade sim-golden | same |
|
||||
| `interfaces#n` | RTL↔SoC interface doc — register-config or data-stream only | write the doc under `work_dir` |
|
||||
| `review:functional_partition#n` | **ask the user** to sign off on the split + interfaces | — (relay to user) |
|
||||
| `module_plan#n` | split the RTL side into A/B/C…, give order + dependencies (**first pass only**) | inline |
|
||||
| `native:<M>#n` | nativize M into HDL-capable blocks, save `<project>_M-E.slx`, generate RTL | `simulink_rtl_dev` |
|
||||
| `ip_replace:<M>#n` | swap in Xilinx IP, cycle-true compare vs MATLAB RTL | `rtl-ip-replace` |
|
||||
| `modify_e:module#n` / `modify_e:vivado#n` / `modify_e:hil#n` | edit **only** the named `-E.slx` (cycle-true / timing / HIL driven) | `simulink_rtl_dev` |
|
||||
| `constraint_primitives#n` | inject the spec's synthesis attributes into the delivered RTL tree; gate = every spec entry hit its target + normalised line diff clean + **re-run byte-identical**. Report `metadata.{injected, unmatched, idempotent_verified, spec_path, spec_hash}` | inline (the injector script + spec; see `Notes/PR23_constraint_primitives.md`) |
|
||||
| `modify_e:module#n` / `modify_e:vivado#n` / `modify_e:hil#n` / `modify_e:constraints#n` | edit **only** the named `-E.slx` (cycle-true / timing / HIL / wrong-signal-shape driven) | `simulink_rtl_dev` |
|
||||
| `sync_sim:<source>#n` | carry that `-E` change into the matching `-x`/`-q` model and re-verify alignment | **`simulink-rtl-redev`** |
|
||||
| `vivado#n` | build the Vivado project, synth+impl+bitstream, check all timing | `vivado-prj-create`, `vivado-mcp` |
|
||||
| `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 |
|
||||
| `hil_rework#n` | apply the code change HIL asked for | inline |
|
||||
| `design_summary#n` | write `<history_dir>/<project_name><YYYYMMDD-HH>_tree.md` (path handed to you in `context.target_path`) covering every `-x`/`-q`/`-E` version, **the `-q` word lengths (multiplier/adder widths, truncation points, loop-accumulator widths) and whether they satisfy the word-length rule** — unrecorded widths force the next run to redo `-q` — **the constraint-primitive spec path and its hash**, which attributes were injected and how many signals each hit (an unrecorded hash forces the next run to re-inject), 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
|
||||
`#2`, `#3`, …
|
||||
@@ -216,6 +332,8 @@ review: {"approved": true, "notes": "", "metadata": {}}
|
||||
|---|---|
|
||||
| `success` + `gate_passed` | every design node — `false` → retry, then `failed` after 10 attempts |
|
||||
| `artifacts` | `simulink_x`'s first artifact **becomes sim-golden**; `vivado`'s go to HIL as the bitstream |
|
||||
| `metadata.wordlength_compliant` + `wordlength_violations` + `wordlengths` | `fixed_point` — a violation (or a bare `wordlength_compliant: false`) fails the gate on its own; `wordlengths` is what the history doc records |
|
||||
| `metadata.injected` + `unmatched` + `idempotent_verified` + `spec_path` + `spec_hash` | `constraint_primitives` — `idempotent_verified: false` fails the gate on its own; **`spec_hash` is what the history doc records**, and without it the next run must re-inject |
|
||||
| `timing_met` | `vivado` only |
|
||||
| `hil_passed` | `hil:*` only |
|
||||
| `needs_code_change` + `change_scope` + `affected_modules` | `vivado` and `hil:*` — the rework router |
|
||||
@@ -238,8 +356,12 @@ A failing `ip_replace:<M>` (cycle-true mismatch) enters the same chain on its ow
|
||||
| stage | keys handed over at once |
|
||||
|---|---|
|
||||
| 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 |
|
||||
| after the module gate | `constraint_primitives#1` — a single global task, never 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`.
|
||||
|
||||
@@ -266,12 +388,16 @@ decisions) to the input JSON; `start`/`resume` strip both.
|
||||
## Other commands
|
||||
|
||||
```bash
|
||||
"$PY" "$RUN" $W status # ledger + pending + final status
|
||||
"$PY" "$RUN" $W status # ledger + pending + final status (read-only)
|
||||
"$PY" "$RUN" schema # both answer templates
|
||||
"$PY" "$RUN" mermaid # graph as Mermaid (see gotcha)
|
||||
"$PY" -m unittest discover -s tests # project's own 16 tests — all pass
|
||||
"$PY" -m unittest discover -s tests # project's own 59 tests — all pass
|
||||
```
|
||||
|
||||
`scripts/migrate_constraint_ledger.py <run_dir> <rounds> [--apply]` back-fills
|
||||
`constraint_primitives#N` placeholders into a ledger written **before** that node
|
||||
existed — see the gotcha below.
|
||||
|
||||
Global flags go **before** the subcommand: `--project`, `--work-dir`, `--thread-id`,
|
||||
`--recursion-limit` (200), `--max-attempts` (10).
|
||||
|
||||
@@ -300,9 +426,30 @@ Global flags go **before** the subcommand: `--project`, `--work-dir`, `--thread-
|
||||
and the graph substitutes `<project_name>-x.slx`. Always record the real filename.
|
||||
- **`module_plan` runs only on the first pass.** Rework rounds reuse the plan, so you
|
||||
will not be asked to re-partition the module list mid-run.
|
||||
- **`constraint_primitives` failing is not a reason to touch the `-E` model.** Record
|
||||
`gate_passed: false` and fix the spec or the injector; the graph retries. Only escalate
|
||||
to `change_scope: "simulink_module"` when the target signal genuinely isn't the shape
|
||||
the spec needs. Getting this backwards turns a spec typo into a model rework round.
|
||||
- **Recording `idempotent_verified: true` without re-running the injector defeats the
|
||||
whole justification.** Idempotence is what makes this stage *not* a hand edit. If you
|
||||
did not actually re-run and compare, record `false`.
|
||||
- **`spec_hash` is the next run's only basis for skipping this stage.** Omit it and the
|
||||
next run re-injects — which is the safe default, not a bug.
|
||||
- **Re-recording a key requires editing `ledger.json` by hand.** `record` only accepts
|
||||
currently-pending keys; anything already in the ledger is replayed forever. To redo a
|
||||
step, delete its entry *and everything after it*, then `resume`.
|
||||
- **Adding a node rewinds every in-flight run.** Replay is keyed by
|
||||
`<task>#<n>`, so a ledger written before `constraint_primitives` existed halts at
|
||||
`constraint_primitives#1` — right after the *first* module round, potentially many
|
||||
rounds back from where the run actually was. The run is not damaged (nothing is
|
||||
deleted), it just asks one extra question per module round already completed. Fix:
|
||||
`scripts/migrate_constraint_ledger.py <run_dir> <number-of-module-rounds> --apply`,
|
||||
which backs the ledger up to `ledger.json.pre_constraint_migration` and seeds one
|
||||
honest placeholder per past round (`migration_placeholder: true`, empty `injected`,
|
||||
empty `spec_hash` — those rounds really did go to Vivado uninjected). The next round
|
||||
gets a real injection. **Rehearse on a copy first**: copy the run dir elsewhere and
|
||||
`resume --work-dir <copy>`; `status` alone cannot prove replay because it never
|
||||
invokes the graph.
|
||||
- **Runs created before the PR01-2 change live in the old location**
|
||||
(`<SIM_RTL_FLOW_ROOT>\runs\<id>\`) and their ledgers have no `review:*` or
|
||||
`module_plan` keys, so they cannot be resumed under the current graph. Start a fresh
|
||||
@@ -311,6 +458,19 @@ Global flags go **before** the subcommand: `--project`, `--work-dir`, `--thread-
|
||||
are fine on Windows; a relative `"."` resolves against wherever the driver was
|
||||
launched — always use an absolute path.
|
||||
- **`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* `xxx<date>_tree.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
|
||||
|
||||
@@ -327,5 +487,6 @@ Global flags go **before** the subcommand: `--project`, `--work-dir`, `--thread-
|
||||
| `needs_code_change=true requires an explicit change_scope` | add `change_scope` |
|
||||
| `MissingReviewGateError` | only if the graph is driven without a review gate — the driver always supplies one |
|
||||
| `device_family must be 'fpga-imx6' or 'zynq-imx6'` | raised in `initialize`; exit 2 |
|
||||
| an old run suddenly asks for `constraint_primitives#1` | its ledger predates the node — run `scripts/migrate_constraint_ledger.py` (see gotcha) |
|
||||
| exit 2 + `recursion_limit` | a rework loop isn't converging — raise `--recursion-limit`, or stop recording `needs_code_change` for the same module |
|
||||
| mojibake in the Chinese prompts | `export PYTHONIOENCODING=utf-8` |
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Seed constraint_primitives#N placeholders into an existing sim-rtl-flow ledger.
|
||||
|
||||
The `constraint_primitives` node was added to the graph after this run started.
|
||||
Replay is keyed by `<task>#<n>`, so a run whose ledger predates the node rewinds
|
||||
to the first unrecorded `constraint_primitives#1` instead of resuming where it
|
||||
left off. Seeding one placeholder per module round that already happened lets
|
||||
the replay walk straight through to the real pending key.
|
||||
|
||||
The placeholders record the truth: those rounds went to Vivado WITHOUT any
|
||||
constraint injection, because the step did not exist yet. The first round that
|
||||
runs after the migration gets a real injection.
|
||||
|
||||
Usage:
|
||||
python migrate_constraint_ledger.py <run_dir> <count> [--apply]
|
||||
Without --apply it only prints what it would do.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PLACEHOLDER = {
|
||||
"success": True,
|
||||
"gate_passed": True,
|
||||
"artifacts": [],
|
||||
"notes": (
|
||||
"迁移占位:这一轮模块处理发生在『约束原语』步骤并入 sim-rtl-flow 之前,"
|
||||
"当时并未做任何综合属性注入,该轮 Vivado 跑的是未注入的 RTL。"
|
||||
"本条只为让 replay 走过历史轮次;之后的每一轮都会真正执行注入。"
|
||||
),
|
||||
"timing_met": True,
|
||||
"hil_passed": True,
|
||||
"needs_code_change": False,
|
||||
"change_scope": "none",
|
||||
"affected_modules": [],
|
||||
"metadata": {
|
||||
"migration_placeholder": True,
|
||||
"injected": {},
|
||||
"unmatched": [],
|
||||
"spec_path": "",
|
||||
"spec_hash": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) < 3:
|
||||
print(__doc__)
|
||||
return 2
|
||||
run_dir = Path(argv[1])
|
||||
count = int(argv[2])
|
||||
apply = "--apply" in argv
|
||||
|
||||
ledger_path = run_dir / "ledger.json"
|
||||
ledger = json.loads(ledger_path.read_text(encoding="utf-8"))
|
||||
|
||||
added = []
|
||||
for n in range(1, count + 1):
|
||||
key = f"constraint_primitives#{n}"
|
||||
if key in ledger:
|
||||
print(f" skip {key} (already recorded)")
|
||||
continue
|
||||
ledger[key] = dict(PLACEHOLDER)
|
||||
added.append(key)
|
||||
|
||||
print(f"ledger: {ledger_path}")
|
||||
print(f"would add: {added}")
|
||||
if not apply:
|
||||
print("(dry run — pass --apply to write)")
|
||||
return 0
|
||||
|
||||
backup = ledger_path.with_suffix(".json.pre_constraint_migration")
|
||||
if not backup.exists():
|
||||
backup.write_text(
|
||||
json.dumps(
|
||||
json.loads(ledger_path.read_text(encoding="utf-8")),
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"backup -> {backup}")
|
||||
ledger_path.write_text(
|
||||
json.dumps(ledger, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
print(f"applied {len(added)} placeholder(s)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv))
|
||||
@@ -119,6 +119,10 @@ def _bootstrap(project: Path):
|
||||
SubagentResult,
|
||||
)
|
||||
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
|
||||
|
||||
return {
|
||||
@@ -128,6 +132,8 @@ def _bootstrap(project: Path):
|
||||
"ReviewDecision": ReviewDecision,
|
||||
"WorkflowContext": WorkflowContext,
|
||||
"build_workflow": build_workflow,
|
||||
"discover_history_doc": discover_history_doc,
|
||||
"history_dir_for": history_dir_for,
|
||||
}
|
||||
|
||||
|
||||
@@ -395,13 +401,37 @@ def _print_final(state: Any, replayed: list[str]) -> None:
|
||||
review = state.get(key)
|
||||
if review:
|
||||
mark = "批准" if review.get("approved") else "驳回"
|
||||
if (review.get("metadata") or {}).get("skipped_by_history"):
|
||||
mark = "按版本说明树文件跳过"
|
||||
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')}")
|
||||
for item in state.get("module_results", []):
|
||||
print(
|
||||
f" - {item['module']}: stage={item['stage']} "
|
||||
f"success={item['success']} artifacts={item.get('artifacts')}"
|
||||
)
|
||||
constraints = state.get("constraint_result") or {}
|
||||
if constraints:
|
||||
print(
|
||||
"约束原语 : "
|
||||
f"source={constraints.get('source')} "
|
||||
f"passed={constraints.get('passed')} "
|
||||
f"spec={constraints.get('spec_path') or '(未回报)'} "
|
||||
f"hash={constraints.get('spec_hash') or '(未回报 — 下轮必须重注)'} "
|
||||
f"injected={_jsonable(constraints.get('injected'))} "
|
||||
f"idempotent={constraints.get('idempotent_verified')}"
|
||||
)
|
||||
print(f"vivado : {_jsonable(state.get('vivado_result'))}")
|
||||
print(f"hil : {_jsonable(state.get('hil_result'))}")
|
||||
print(f"\nevents ({len(state.get('events', []))}):")
|
||||
@@ -452,7 +482,20 @@ def _invoke(run: RunDir, api: dict[str, Any], args) -> int:
|
||||
def cmd_start(args, api) -> int:
|
||||
payload = json.loads(Path(args.input).read_text(encoding="utf-8"))
|
||||
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:本次运行结束时会写出一份
|
||||
# 新的 xxx<date>_tree.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, payload.get("project_name", "")
|
||||
)
|
||||
run = RunDir(work_dir, args.thread_id)
|
||||
run.set_input(payload)
|
||||
if not args.keep_ledger:
|
||||
run.set_ledger({})
|
||||
@@ -460,6 +503,10 @@ def cmd_start(args, api) -> int:
|
||||
print(f"work dir : {run.work_dir}")
|
||||
print(f"run dir : {run.path}")
|
||||
print(f"thread : {run.thread_id}")
|
||||
print(
|
||||
"history : "
|
||||
+ (payload.get("history_path") or "(none — 首次设计,全部步骤实打实执行)")
|
||||
)
|
||||
print("input : " + json.dumps(payload, ensure_ascii=False))
|
||||
return _invoke(run, api, args)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ FPGA 做 8VSB 调制 / DPD / AGC / TS 前处理,imx6 跑 `atsc_app`(配置 A
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 激励器 Vivado 工程 | `E:\Xilinx_design\smod\SMOD_ATSC\my_exciter\my_exciter.xpr`(top = `smod_top`,也是 **git 仓库根**) |
|
||||
| 激励器 Vivado 工程 | `E:\Xilinx_design\smod\SMOD_ATSC\my_exciter\my_exciter.xpr`(top = `smod_top`,也是 **git 仓库根**;脚本取 `~\skills_env.md` 的 `SMOD_REPO_ROOT`) |
|
||||
| 打底 golden 工程 | `E:\Xilinx_design\smod\SMOD_ATSC\RU_golden\RU_golden.xpr`(top = `RU_golden_top`) |
|
||||
| FPGA / Flash | `xc7a200tfbg676-2` / Winbond **w25q128bv**(128 Mbit = 16 MB,SPIx1) |
|
||||
| Vivado | 见 `~\skills_env.md` 的 `VIVADO_ROOT`(其下 `bin\vivado.bat`)——路径因机器而异,不要写死盘符 |
|
||||
|
||||
@@ -52,7 +52,10 @@ def _env_lookup(key, default=None):
|
||||
return default
|
||||
|
||||
|
||||
REPO = r"E:\Xilinx_design\smod\SMOD_ATSC\my_exciter"
|
||||
# Machine-relative: the exciter checkout lives wherever this box keeps it.
|
||||
# Recorded as SMOD_REPO_ROOT in ~/skills_env.md; the literal stays as the
|
||||
# fallback so existing boxes keep working without touching that file.
|
||||
REPO = _env_lookup("SMOD_REPO_ROOT", r"E:\Xilinx_design\smod\SMOD_ATSC\my_exciter")
|
||||
APP_SUBDIR = "imx6/src/atsc_app"
|
||||
|
||||
# No default: the VM's IP moves with the VMware segment, so a stale fallback would
|
||||
|
||||
@@ -54,6 +54,15 @@ powershell -File <skill目录>\scripts\sync_repos.ps1 -RepoUrl <origin_url>
|
||||
|
||||
- **rebase 冲突**:逐个文件看冲突双方,skills 文本冲突通常保留两边语义合并;
|
||||
解不了就 `git rebase --abort` 并向用户报告冲突文件清单,让用户裁决。
|
||||
- **fetch / push 报 `RPC failed; HTTP 504`(服务端 pack 进程卡死)**:先分清哪一半坏了 ——
|
||||
`git ls-remote` 与 Gitea HTTP API 正常、只有 `git-upload-pack` / `git-receive-pack` 504,
|
||||
就不是认证也不是网络。**`--depth=1` 不能绕过,POST 体只有几十 KB 也照样 504**,所以
|
||||
不要在"减小传输量"上浪费时间。此时若远端有本机没有的 commit(无法 rebase 就无法 push),
|
||||
用 `scripts/recover_commits_via_gitea_api.py <repo_dir> <sha>...` 按**父在前**的顺序
|
||||
经 API 逐字节重建那几个 commit:它只在重建对象的 SHA-1 与服务端报的 sha 完全一致时才
|
||||
写入,所以是可证同一份历史,不是近似复制。重建后
|
||||
`git update-ref refs/remotes/origin/main <sha>` + `git rebase <sha>`,本地历史就归位了,
|
||||
服务恢复后一条 `git push` 即可。**任何情况下都不要 force push**(会丢掉别的机器的 commit)。
|
||||
- **push 被拒(认证)**:检查凭据管理器条目;新机器上用
|
||||
`git config --global credential.helper manager` 后重试一次,让 credential
|
||||
helper 弹窗或从 skills_env.md 读凭据拼一次性 URL 完成首次认证。
|
||||
|
||||
@@ -24,11 +24,22 @@ CONDA_ROOT = C:\ProgramData\miniconda3
|
||||
CONDA_LANGGRAPH_PYTHON = C:\Users\<user>\.conda\envs\langgraph\python.exe
|
||||
```
|
||||
|
||||
## LangGraph 工作流仓库(各 flow skill 的 driver 从这里取项目根)
|
||||
|
||||
```
|
||||
LANGGRAPH_DESIGN_ROOT = <LangGraph 工程总目录,如 E:\LangGraph_Design>
|
||||
SIM_RTL_FLOW_ROOT = {LANGGRAPH_DESIGN_ROOT}\sim-rtl-flow
|
||||
ZYNQ_IMX6_FLOW_ROOT = {LANGGRAPH_DESIGN_ROOT}\zynq-imx6-flow
|
||||
FPGA_IMX6_FLOW_ROOT = {LANGGRAPH_DESIGN_ROOT}\FPGA-imx6-flow
|
||||
FPGA_GOLDENREF_ROOT = {LANGGRAPH_DESIGN_ROOT}pga-goldenref-flow
|
||||
```
|
||||
|
||||
## EDA / 硬件工具
|
||||
|
||||
```
|
||||
VIVADO_ROOT = <Vivado 安装根目录,如 C:\Xilinx\Vivado\2018.1 —— 必须实测,不同机器盘符不同>
|
||||
VIVADO_PATH = <上一行 + \bin\vivado.bat>
|
||||
SDK_ROOT = <Xilinx SDK 安装根,如 D:\Xilinx\SDK8.1;含 binootgen.bat / bin\program_flash.bat>
|
||||
KICAD_PYTHONPATH = C:\Program Files\KiCad\10.0\bin\Lib\site-packages
|
||||
MATLAB_ROOT = <MATLAB 安装根目录,如 D:\MATLAB\R2025b>
|
||||
MATLAB_AGENTIC_ROOT = C:\Users\<user>\.matlab\agentic-toolkits
|
||||
@@ -42,6 +53,19 @@ N9320B_HOST = 192.168.1.147 # Keysight N9320B 频谱仪
|
||||
SMOD_BOARD_IP = 192.168.1.143 # SMOD 板(ATSC 激励器远程升级)
|
||||
```
|
||||
|
||||
## SMOD imx6 设备与交叉编译 VM(smod_atsc_dev / fpga_update 使用)
|
||||
|
||||
```
|
||||
SMOD_REPO_ROOT = <ATSC 激励器 Vivado 工程根,同时是 git 仓库根>
|
||||
SMOD_SSH_USER = root # imx6 SSH/网页登录名(老 dropbear)
|
||||
SMOD_SSH_PASS = <填写>
|
||||
SMOD_WEB_USER = <网页 Remote Update 账号,与 SSH 不同>
|
||||
SMOD_WEB_PASS = <填写>
|
||||
CROSS_VM_HOST = <交叉编译 VM 的 IP —— 随 VMware 网段变化,必须实测>
|
||||
CROSS_VM_USER = <填写>
|
||||
CROSS_VM_PASS = <填写>
|
||||
```
|
||||
|
||||
## MCP 注册配置(真源:MCP_DESIGN_ROOT\mcp_list.md)
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Rebuild commits the git transport cannot fetch, using the Gitea HTTP API.
|
||||
|
||||
Why this exists: git.rpchannel.com has repeatedly served `info/refs` and the
|
||||
whole HTTP API fine while `git-upload-pack` / `git-receive-pack` 504 (the
|
||||
server-side git process hangs -- a 32KB push body 504s just as a full fetch
|
||||
does). That leaves the local checkout unable to rebase onto a remote head it
|
||||
can see but cannot download.
|
||||
|
||||
The API exposes the same objects the pack protocol would have shipped, so the
|
||||
missing commits can be rebuilt locally:
|
||||
|
||||
/api/v1/repos/<o>/<r>/git/commits/<sha> author, committer, message, parents
|
||||
/api/v1/repos/<o>/<r>/git/trees/<sha>?recursive=1 every path, mode and blob sha
|
||||
/api/v1/repos/<o>/<r>/git/blobs/<sha> base64 content
|
||||
|
||||
This is a reconstruction, not a copy, so it is only trustworthy because it is
|
||||
self-verifying: every blob is accepted only if its SHA-1 matches what the
|
||||
server reported, and a commit is written only if the assembled object hashes to
|
||||
the exact sha being recovered. A mismatch aborts instead of writing something
|
||||
plausible.
|
||||
|
||||
Two traps, both of which produce a silently wrong result rather than an error:
|
||||
* `git update-index --index-info` input must be bytes. With text=True on
|
||||
Windows the newlines become CRLF, every line is rejected, and write-tree
|
||||
happily returns the EMPTY tree (4b825dc6...).
|
||||
* Gitea fills the commit's `tree.sha` field with the COMMIT sha. Do not use it
|
||||
as a check -- verify the final commit sha instead.
|
||||
|
||||
Usage:
|
||||
python recover_commits_via_gitea_api.py <repo_dir> <sha> [<sha> ...]
|
||||
|
||||
Recover in parent-first order. Afterwards the shas are real local objects:
|
||||
git update-ref refs/remotes/origin/<branch> <newest_sha>
|
||||
git rebase <newest_sha>
|
||||
"""
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
|
||||
def api_base(repo_dir):
|
||||
"""Derive the Gitea API root from origin's URL."""
|
||||
url = subprocess.run(("git", "-C", repo_dir, "remote", "get-url", "origin"),
|
||||
capture_output=True, text=True, check=True).stdout.strip()
|
||||
m = re.match(r"https?://([^/]+)/([^/]+)/([^/]+?)(?:\.git)?/?$", url)
|
||||
if not m:
|
||||
sys.exit(f"cannot parse a Gitea owner/repo out of origin url: {url}")
|
||||
host, owner, repo = m.groups()
|
||||
return f"http://{host}/api/v1/repos/{owner}/{repo}"
|
||||
|
||||
|
||||
def get(url):
|
||||
with urllib.request.urlopen(url, timeout=90) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def git_date(iso):
|
||||
"""'2026-07-30T22:19:56+08:00' -> ('1785...', '+0800')"""
|
||||
dt = datetime.datetime.fromisoformat(iso)
|
||||
total = int(dt.utcoffset().total_seconds())
|
||||
sign = "+" if total >= 0 else "-"
|
||||
total = abs(total)
|
||||
return str(int(dt.timestamp())), f"{sign}{total // 3600:02d}{(total % 3600) // 60:02d}"
|
||||
|
||||
|
||||
def recover(sha, base, env, index_file):
|
||||
meta = get(f"{base}/git/commits/{sha}")
|
||||
tree = get(f"{base}/git/trees/{sha}?recursive=true&per_page=1000")
|
||||
if tree.get("truncated"):
|
||||
sys.exit(f"{sha[:8]}: tree listing truncated -- raise per_page")
|
||||
|
||||
blobs = [e for e in tree["tree"] if e["type"] == "blob"]
|
||||
fetched = 0
|
||||
for e in blobs:
|
||||
present = subprocess.run(("git", "cat-file", "-e", e["sha"] + "^{object}"),
|
||||
env=env, capture_output=True).returncode == 0
|
||||
if present:
|
||||
continue
|
||||
raw = base64.b64decode(get(f"{base}/git/blobs/{e['sha']}")["content"])
|
||||
got = hashlib.sha1(b"blob %d\0" % len(raw) + raw).hexdigest()
|
||||
if got != e["sha"]:
|
||||
sys.exit(f"blob mismatch for {e['path']}: {got} != {e['sha']}")
|
||||
w = subprocess.run(("git", "hash-object", "-t", "blob", "-w", "--stdin"),
|
||||
env=env, input=raw, capture_output=True)
|
||||
if w.stdout.decode().strip() != e["sha"]:
|
||||
sys.exit(f"write-back drift for {e['path']}")
|
||||
fetched += 1
|
||||
|
||||
if os.path.exists(index_file):
|
||||
os.remove(index_file)
|
||||
payload = "".join(f"{e['mode']} {e['sha']}\t{e['path']}\n" for e in blobs)
|
||||
r = subprocess.run(("git", "update-index", "--index-info"), env=env,
|
||||
input=payload.encode("utf-8"), capture_output=True)
|
||||
if r.returncode:
|
||||
sys.exit(f"update-index failed: {r.stderr.decode()}")
|
||||
tree_sha = subprocess.run(("git", "write-tree"), env=env,
|
||||
capture_output=True, text=True, check=True).stdout.strip()
|
||||
|
||||
c = meta["commit"]
|
||||
ats, atz = git_date(c["author"]["date"])
|
||||
cts, ctz = git_date(c["committer"]["date"])
|
||||
head = f"tree {tree_sha}\n"
|
||||
for p in meta.get("parents", []):
|
||||
head += f"parent {p['sha']}\n"
|
||||
head += (f"author {c['author']['name']} <{c['author']['email']}> {ats} {atz}\n"
|
||||
f"committer {c['committer']['name']} <{c['committer']['email']}> {cts} {ctz}\n\n")
|
||||
|
||||
msg = c["message"]
|
||||
for variant in (msg, msg + "\n", msg.rstrip("\n") + "\n", msg.rstrip("\n")):
|
||||
body = (head + variant).encode("utf-8")
|
||||
if hashlib.sha1(b"commit %d\0" % len(body) + body).hexdigest() == sha:
|
||||
w = subprocess.run(("git", "hash-object", "-t", "commit", "-w", "--stdin"),
|
||||
env=env, input=body, capture_output=True)
|
||||
if w.stdout.decode().strip() != sha:
|
||||
sys.exit(f"{sha[:8]}: commit write drift")
|
||||
print(f"OK {sha[:8]} tree={tree_sha[:8]} blobs_fetched={fetched}")
|
||||
return True
|
||||
print(f"FAIL {sha[:8]} no reconstruction hashes to this sha "
|
||||
f"(signed commit, or the API is reporting different metadata)")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
sys.exit(__doc__)
|
||||
repo_dir, shas = sys.argv[1], sys.argv[2:]
|
||||
git_dir = subprocess.run(("git", "-C", repo_dir, "rev-parse", "--absolute-git-dir"),
|
||||
capture_output=True, text=True, check=True).stdout.strip()
|
||||
index_file = os.path.join(os.environ.get("TEMP", "."), "recover_gitea_index")
|
||||
# A private index keeps the caller's working tree and real index untouched.
|
||||
env = dict(os.environ, GIT_DIR=git_dir, GIT_INDEX_FILE=index_file)
|
||||
base = api_base(repo_dir)
|
||||
ok = all(recover(s, base, env, index_file) for s in shas)
|
||||
if os.path.exists(index_file):
|
||||
os.remove(index_file)
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user