Compare commits

...
8 Commits
Author SHA1 Message Date
bowei 120dc66925 sync: SHANGHAI-U9 2026-08-26 fpga_update/gen_update_mcs.py 的 REPO 改读 skills_env.md 的 SMOD_REPO_ROOT(0819 遗漏的第三个文件) 2026-08-26 07:50:48 +08:00
bowei 307bc38d36 sync: SHANGHAI-U9 2026-08-19 tools-sync 增 504 恢复工具 recover_commits_via_gitea_api.py 并写进阶段1处置 2026-08-19 15:16:56 +08:00
bowei 28307b12f6 sync: SHANGHAI-U9 2026-08-19 skills_env 模板补回漂移的 12 个键(LangGraph flow 根 / SDK_ROOT / SMOD+VM 段,凭据一律占位符) 2026-08-19 12:58:21 +08:00
bowei effa9361ac sync: SHANGHAI-U9 2026-08-19 硬编码审计:imx6_app REPO / fpga_ru --host 改读 skills_env.md(新增 SMOD_REPO_ROOT) 2026-08-19 12:56:36 +08:00
bowei 6535dd83d4 sync: SHANGHAI-U9 2026-08-19 sim-rtl-flow 增约束原语阶段与版本说明树文件重命名,新增 ledger 迁移脚本 2026-08-19 12:50:44 +08:00
bowei d77d025c59 sync: SHANGHAI-U9 2026-08-12 2026-08-19 12:50:44 +08:00
boweiandClaude Opus 5 0b5583a5c0 sync: WIN-BOWEI 2026-08-10 硬编码审计:两个 flow driver 的工程根改读 skills_env.md
- run_fpga_local.py: FPGA_FLOW_ROOT 写死 E:\LangGraph_Design\fpga-goldenref-flow
  -> _env_lookup(FPGA_GOLDENREF_ROOT),未配置时给出明确提示。
- run_imx6_flow.py: DEFAULT_PROJECT 写死 -> _env_lookup(FPGA_IMX6_FLOW_ROOT),
  --project-dir 为空时报错而不是拿错路径继续。
与 sim-rtl-flow / zynq-imx6-flow 已有写法一致。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 12:50:44 +08:00
boweiandClaude Opus 5 e07cfaa6d1 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>
2026-08-19 12:50:44 +08:00
19 changed files with 1191 additions and 56 deletions
+29 -1
View File
@@ -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)
+30 -1
View File
@@ -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
View File
@@ -1,13 +1,13 @@
---
---
name: fpga_flash
description: |
烧写 Artix-7 系列 FPGA 的外部 Flash(SPI NOR Flash)。适用于 EMV7.1 等 Artix-7 板卡,支持 Micron mt25ql128 和 Winbond w25q128bv 两种 Flash 型号。
按开发板/器件架构选择正确的 Flash 固化方式。支持 EMV7.1 等 Artix-7 板卡的 MCS/cfgmem 流程,以及 HJX9009(XC7Z100/Zynq-7000)将 FSBL、PL bitstream、PS ELF 打包为 BOOT.bin 后写入双并行 QSPI 的流程。
当用户提到"烧写 flash"、"flash 编程"、"固化 bit 文件"、"下载到 flash"、"program flash"、"write flash"、"burn flash"、"Vivado flash"、"SPI flash 烧写"时,必须触发此 skill。
---
# FPGA Flash 烧写流程(Artix-7 / EMV7.1)
# FPGA Flash 固化流程(按开发板路由)
本 skill 由 Claude **全自动执行**完整的 Flash 烧写流程。除了向用户确认必要信息外,所有操作(文件检查、CSV 修改、Vivado 命令执行、硬件编程)均由 Claude 直接完成,不要求用户手动执行任何命令。
本 skill 由 Claude **全自动执行**完整的 Flash 固化流程。除了向用户确认必要信息外,所有操作(文件检查、备份、哈希/容量/内容守卫、工具调用、硬件编程)均由 Claude 直接完成,不要求用户手动执行任何命令。执行前必须先识别开发板与 FPGA/SoC 类型;不得把 Artix-7 的 `.mcs/cfgmem` 流程用于 Zynq,也不得把 Zynq 的 `BOOT.bin/program_flash` 流程用于普通 Artix-7。
---
@@ -34,6 +34,9 @@ scripts/
program_flash.py # 第四、五步:连接目标板并烧写 Flash
step3_write_mcs.tcl # gen_mcs.py 使用的 Tcl 模板(勿手动修改)
step4_5_program.tcl # program_flash.py 使用的 Tcl 模板(勿手动修改)
build_zynq_boot.py # Zynq:FSBL + bit + PS ELF 生成受保护的 BOOT.bin
program_zynq_qspi.py # Zynq:哈希/容量/marker 守卫后烧写并强制 verify
vivado_env.py # 解析 Vivado/SDK 工具路径
```
**调用方式(Bash 工具):**
@@ -41,10 +44,153 @@ scripts/
python "$USERPROFILE/.claude/skills/fpga_flash/scripts/<script>.py" [参数...]
```
各脚本通过命令行参数接受变量(Vivado 安装目录、工程名、bit 文件路径、mcs 文件路径等),内部调用 Vivado `-mode batch -source <tcl模板> -tclargs <参数>` 完成执行,全程不产生临时文件。
各脚本通过命令行参数接受变量(Vivado/SDK 安装目录、bit/ELF/MCS/BOOT 路径、容量和 marker 等)。Artix-7 脚本内部调用 Vivado `-mode batch -source <tcl模板> -tclargs <参数>`;Zynq 脚本调用 SDK 的 `bootgen`/`program_flash`。工程路径和板级文件均由参数传入,不在 skill 中硬编码。
---
## 第零步:识别板卡并选择唯一流程
先用工程名、器件型号、原理图/板卡说明和现有部署资料交叉确认,不允许只根据输出文件扩展名猜测。
| 板卡/器件 | 固化容器 | 写入工具与接口 | 选择的流程 |
|---|---|---|---|
| HJX9009、`xc7z100`、Zynq-7000(PS+PL) | `BOOT.bin`,顺序为启动 FSBL + PL `.bit` + PS `.elf` | SDK `bootgen` + `program_flash -flash_type qspi_dual_parallel` | 本文“Zynq/HJX9009”流程 |
| EMV7.1、`xc7a*`、Artix-7(纯 PL) | `.mcs` | Vivado `write_cfgmem` + Hardware Manager cfgmem | 本文“Artix-7/EMV7.1”流程 |
硬阻断规则:
- 发现 `xc7z*`、PS7、FSBL、PS ELF 或工程明确写明 Zynq 时,禁止运行 `gen_mcs.py`、`program_flash.py` 或 `create_hw_cfgmem`。
- 发现 `xc7a*` 且无 PS/FSBL 时,禁止生成 `BOOT.bin`。
- 板型、Flash 拓扑或容量不明确时,先查原理图/现有已验证部署记录;不得试烧。
- HJX9009 已验证为两片 `s25fl128s` 双并行 QSPI,总容量 32 MiB,必须使用 `qspi_dual_parallel`,不能退化为 `qspi_single`。
---
## Zynq/HJX9009:同时固化 PS 与 PL
### Z1. 输入与两个 FSBL 角色
生成冷启动镜像需要三个输入,BIF 中顺序不可改变:
```text
the_ROM_image:
{
[bootloader]<boot_fsbl.elf>
<implemented_pl.bit>
<ps_application.elf>
}
```
必须区分两个角色不同的 FSBL:
- **启动 FSBL**:封装在 `BOOT.bin` 第一分区,板卡冷启动时由 BootROM 加载,随后配置 PL 并启动 PS 应用。
- **JTAG 烧写辅助 FSBL**:由 `program_flash -fsbl` 临时下载到 OCM,用于初始化当前板卡的 QSPI MIO/控制器并完成写入。它不等于、也不能默认替换 `BOOT.bin` 内的启动 FSBL。
对 HJX9009,必须使用已验证能初始化该板 QSPI MIO 的参考 JTAG 辅助 FSBL。若项目提供其期望 SHA-256/MD5,必须作为守卫验证。错误的辅助 FSBL 可能读出 JEDEC `00,00,00`,造成表面执行但没有可靠写入。
HJX9009 当前工程部署记录中的参考辅助 FSBL 指纹为 MD5 `14232757864453B418E67D63457EE24F`。这只是板级身份参考,不应写死进通用脚本;实际执行仍应从当前项目部署清单取得文件并同时记录 SHA-256。
### Z2. 安全预检与备份顺序
执行顺序固定如下:
1. 确认本次 PS ELF、时序通过的 PL bitstream、启动 FSBL 和 JTAG 辅助 FSBL 的绝对路径。
2. 记录四个输入的 SHA-256、字节数和修改时间;若有 `.ltx`,另记录其 SHA-256,确保它与 bitstream 同次构建。
3. 在覆盖部署目录中的任何已有 `BOOT.bin`、BIF、FSBL、bit 或 ELF 前,复制到带时间戳的备份目录并写 manifest。不得删除已知可回退镜像。
4. 对源文件和 staging 副本逐个比较 SHA-256,任一不一致立即停止。
5. 为新固件指定至少一个能区分版本的 ASCII marker,并在 PS ELF 和生成的 `BOOT.bin` 中检查。marker 不匹配立即停止。
6. 生成后要求 `BOOT.bin` 严格小于物理 QSPI 容量;HJX9009 上限为 33,554,432 字节。
7. 记录最终 `BOOT.bin` 的 SHA-256 和精确长度;烧写命令必须重新校验这两个值,防止旧镜像或 TOCTOU 误烧。
### Z3. 生成 BOOT.bin
调用参数化脚本;示例只表示参数结构,实际路径和 marker 必须来自当前项目。
Xilinx SDK 安装根目录由脚本自行解析(`SDK_ROOT` / `SDK_PATH`:先查环境变量,再查
`~\skills_env.md`),只有需要临时覆盖时才显式传 `--sdk-dir`:
```powershell
python "$env:USERPROFILE\.claude\skills\fpga_flash\scripts\build_zynq_boot.py" `
--boot-fsbl "<cold-boot-fsbl.elf>" `
--bit-file "<implemented-top.bit>" `
--app-elf "<ps-application.elf>" `
--boot-bin "<deployment-dir>\BOOT.bin" `
--bif-file "<deployment-dir>\boot.bif" `
--capacity-bytes 33554432 `
--require-marker "<build-identity-marker>"
```
脚本会备份已有 BOOT/BIF、生成候选镜像、执行容量与 marker 守卫、原子替换最终镜像,并输出 JSON manifest。成功标志为:
```text
ZYNQ_BOOTGEN_PASS size=<n> sha256=<64 hex>
```
如果工程采用 staging 目录,复制 FSBL/bit/ELF 后仍要逐项重算并对比 SHA-256。`BOOT.bin` 约 18 MiB 只是 HJX9009 的常见值,不是成功判据。
### Z4. 启动 Vivado hw_server 并确认 JTAG
HJX9009 当前机器上必须由 **Vivado** 的 `hw_server` 给 Platform Cable USB II 加载固件;SDK 自带 hw_server 可能无法完成该动作。先启动并等待枚举出 `xc7z100`:
```powershell
& '<Vivado-root>\bin\hw_server.bat' -s tcp::3121
```
保持服务运行,确认 `tcp:127.0.0.1:3121` 可连接且 JTAG 链中只有预期目标。若 3121 被未知进程占用、目标器件不是 `xc7z100`、板卡掉电或线缆未枚举,停止,不得烧写。
### Z5. 写入 HJX9009 双并行 QSPI 并强制 verify
从 Z3 的输出/manifest 复制**精确** SHA-256 和长度后执行:
```powershell
python "$env:USERPROFILE\.claude\skills\fpga_flash\scripts\program_zynq_qspi.py" `
--image "<deployment-dir>\BOOT.bin" `
--jtag-fsbl "<verified-jtag-helper-fsbl.elf>" `
--expect-sha256 "<exact-BOOT.bin-sha256>" `
--expect-size <exact-byte-length> `
--capacity-bytes 33554432 `
--flash-type qspi_dual_parallel `
--hw-server-url tcp:127.0.0.1:3121 `
--expect-jtag-fsbl-sha256 "<exact-helper-FSBL-sha256>" `
--require-marker "<build-identity-marker>" `
--log-file "<deployment-dir>\program_flash.log"
```
安全约束:
- offset 必须为 `0`;HJX9009 的 Flash 类型必须保持 `qspi_dual_parallel`。
- 参数顺序必须是 `-verify` 在 `-fsbl` 前,`-cable ...` 最后;参数化脚本已固定此顺序。
- 擦除、编程、回读校验可能超过 10 分钟;期间禁止断电、拔 JTAG、关闭 hw_server 或终止进程。
- 不信任包装层退出码。已验证存在“无法连接 hw_server 但返回码仍为 0”的情况。
真正成功必须在同一次日志中同时出现:
```text
Erase Operation successful
Program Operation successful
Verify Operation successful
Flash Operation Successful
ZYNQ_QSPI_FLASH_VERIFY_PASS
```
若出现 `Verify Operation unsuccessful`、`Flash Operation Failed`,或缺少任一成功行,一律判失败。`End of startup status: HIGH` 只证明易失配置完成,不能证明 Flash 已写入。
### Z6. 完全断电冷启动与 COM5 验收
烧写成功后还必须验证自主启动:
1. 保存烧写日志、BOOT manifest、bit/LTX/ELF 哈希和回退镜像位置。
2. 关闭板卡主电源及可能通过 JTAG 反向供电的接口,静置至少 30 秒;JTAG 枚举异常时按板级规程全断 90 秒。
3. 保持 JTAG 断开,重新打开主电源,让板卡只从 QSPI 启动。
4. 打开 COM5(115200、8N1、无流控),保存带时间戳的启动日志。
5. 要求看到本次 PS 固件的唯一 marker/banner,并执行项目定义的只读状态命令。HJX9009 当前固件通常用 `HELP`、`?`,随后按项目规程输入 `R`;验收期望状态和命令必须以本次固件文档为准。
6. 若 marker 不匹配、COM5 无输出、PL/PS 版本不一致或只能在重新接入 JTAG 后工作,判定冷启动失败并使用已备份的已知良好镜像回退。
只有“写入日志完整成功 + verify 成功 + 完全断电后 COM5 确认本次固件自主启动”三者同时成立,才可以向用户报告 HJX9009 PS+PL 已固化。
---
## Artix-7/EMV7.1:MCS/cfgmem 流程(保留)
## 第一步:检查并修复 Vivado Flash 器件表
**Claude 自动执行:**
@@ -156,3 +302,6 @@ ILA core 的 `Dropping logic core` WARNING 是正常现象(载入 loader/golde
| DONE 灯不亮 | 启动模式跳线错误 | 检查 MODE 引脚电平,确认为 SPI 启动模式 |
| 烧写后板卡跑旧程序 | Flash 未完全擦除 | 确认 `PROGRAM.ERASE 1` 已设置后重新执行 |
| `PermissionError` 写 CSV | 权限不足 | 提示用户以管理员身份运行 Claude Code |
| Zynq `program_flash` 返回 0 但无成功行 | hw_server 未连接或工具包装层假成功 | 按日志判失败;要求 erase/program/verify/Flash 四组成功标志齐全 |
| HJX9009 JEDEC 为 `00,00,00` | JTAG 辅助 FSBL 未正确初始化 QSPI MIO | 使用项目已验证的参考辅助 FSBL并核对哈希,保持 `qspi_dual_parallel` |
| `BOOT.bin` 可写但冷启动无 COM5 | 启动 FSBL/PS ELF/PL 不匹配或未自主启动 | 完全断电、断开 JTAG,从 QSPI 冷启动并核对固件 marker;失败则回退备份 |
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""Build a guarded Zynq-7000 BOOT.bin from FSBL + PL bit + PS ELF.
The output is generated as a candidate, checked for capacity and required
ASCII build markers, then atomically promoted. Existing BOOT.bin/BIF files are
backed up before replacement. This script does not program hardware.
"""
import argparse
import datetime as dt
import hashlib
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
from vivado_env import resolve_sdk_dir
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest().upper()
def require_file(label: str, value: str) -> Path:
path = Path(value).expanduser().resolve()
if not path.is_file():
raise SystemExit(f"ERROR: missing {label}: {path}")
return path
def bif_path(path: Path) -> str:
return path.as_posix()
def main() -> None:
parser = argparse.ArgumentParser(
description="Build guarded Zynq BOOT.bin (boot FSBL + PL bit + PS ELF)"
)
parser.add_argument("--sdk-dir", help="Xilinx SDK root containing bin/bootgen.bat")
parser.add_argument("--boot-fsbl", required=True, help="FSBL executed at cold boot")
parser.add_argument("--bit-file", required=True, help="implemented PL .bit file")
parser.add_argument("--app-elf", required=True, help="PS application ELF")
parser.add_argument("--boot-bin", required=True, help="final BOOT.bin path")
parser.add_argument("--bif-file", help="persistent BIF path; default beside BOOT.bin")
parser.add_argument("--capacity-bytes", type=int, default=32 * 1024 * 1024)
parser.add_argument(
"--require-marker", action="append", default=[],
help="ASCII marker that must exist in the generated image; repeatable",
)
parser.add_argument(
"--backup-dir",
help="backup root; default <BOOT.bin directory>/flash_backups",
)
parser.add_argument("--dry-run", action="store_true", help="validate and print only")
args = parser.parse_args()
boot_fsbl = require_file("boot FSBL", args.boot_fsbl)
bit_file = require_file("PL bitstream", args.bit_file)
app_elf = require_file("PS application ELF", args.app_elf)
boot_bin = Path(args.boot_bin).expanduser().resolve()
bif_file = (
Path(args.bif_file).expanduser().resolve()
if args.bif_file else boot_bin.with_suffix(".bif")
)
if args.capacity_bytes <= 0:
raise SystemExit("ERROR: --capacity-bytes must be positive")
try:
marker_bytes = [(marker, marker.encode("ascii")) for marker in args.require_marker]
except UnicodeEncodeError:
raise SystemExit("ERROR: --require-marker values must be ASCII")
app_bytes = app_elf.read_bytes()
missing_app_markers = [marker for marker, raw in marker_bytes if raw not in app_bytes]
if missing_app_markers:
raise SystemExit(
"ERROR: PS application ELF lacks required markers: " + repr(missing_app_markers)
)
sdk_dir = Path(resolve_sdk_dir(args.sdk_dir)).expanduser().resolve()
bootgen = sdk_dir / "bin" / "bootgen.bat"
if not bootgen.is_file():
raise SystemExit(f"ERROR: bootgen not found: {bootgen}")
bif_text = (
"the_ROM_image:\n"
"{\n"
f" [bootloader]{bif_path(boot_fsbl)}\n"
f" {bif_path(bit_file)}\n"
f" {bif_path(app_elf)}\n"
"}\n"
)
candidate = boot_bin.with_name(f"{boot_bin.name}.candidate.{os.getpid()}")
cmd = [
str(bootgen), "-image", str(bif_file), "-arch", "zynq",
"-o", str(candidate), "-w", "on",
]
print(f"[build_zynq_boot] boot FSBL: {boot_fsbl} sha256={sha256(boot_fsbl)}")
print(f"[build_zynq_boot] PL bit : {bit_file} sha256={sha256(bit_file)}")
print(f"[build_zynq_boot] PS ELF : {app_elf} sha256={sha256(app_elf)}")
print(f"[build_zynq_boot] output : {boot_bin}")
print(f"[build_zynq_boot] capacity : {args.capacity_bytes}")
print("[build_zynq_boot] command : " + subprocess.list2cmdline(cmd))
if args.dry_run:
print("DRY_RUN_OK: no files changed and bootgen was not executed")
return
boot_bin.parent.mkdir(parents=True, exist_ok=True)
bif_file.parent.mkdir(parents=True, exist_ok=True)
stamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
backup_root = (
Path(args.backup_dir).expanduser().resolve()
if args.backup_dir else boot_bin.parent / "flash_backups"
)
existing = [path for path in (boot_bin, bif_file) if path.exists()]
if existing:
backup = backup_root / stamp
backup.mkdir(parents=True, exist_ok=False)
for path in existing:
shutil.copy2(path, backup / path.name)
print(f"[build_zynq_boot] backup : {backup}")
bif_file.write_text(bif_text, encoding="utf-8", newline="\n")
try:
result = subprocess.run(cmd, check=False)
if result.returncode != 0 or not candidate.is_file():
raise RuntimeError(f"bootgen failed, rc={result.returncode}")
size = candidate.stat().st_size
if size >= args.capacity_bytes:
raise RuntimeError(
f"candidate size {size} is not below capacity {args.capacity_bytes}"
)
image = candidate.read_bytes()
missing = [marker for marker, raw in marker_bytes if raw not in image]
if missing:
raise RuntimeError("candidate lacks required markers: " + repr(missing))
candidate_hash = sha256(candidate)
os.replace(candidate, boot_bin)
except Exception as exc:
if candidate.exists():
candidate.unlink()
raise SystemExit(f"ERROR: {exc}")
manifest = {
"format": "zynq-7000-boot-bin",
"created": dt.datetime.now().astimezone().isoformat(),
"capacity_bytes": args.capacity_bytes,
"required_markers": args.require_marker,
"components": {
"boot_fsbl": {"path": str(boot_fsbl), "sha256": sha256(boot_fsbl)},
"pl_bit": {"path": str(bit_file), "sha256": sha256(bit_file)},
"ps_elf": {"path": str(app_elf), "sha256": sha256(app_elf)},
},
"boot_bin": {"path": str(boot_bin), "size": size, "sha256": candidate_hash},
}
manifest_path = boot_bin.with_suffix(boot_bin.suffix + ".manifest.json")
manifest_path.write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
print(f"ZYNQ_BOOTGEN_PASS size={size} sha256={candidate_hash}")
print(f"MANIFEST={manifest_path}")
if __name__ == "__main__":
main()
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""Guard and program a Zynq BOOT.bin through Xilinx program_flash.
Success requires erase, program, verify, and the final Xilinx success marker;
the process exit code alone is deliberately not trusted.
"""
import argparse
import hashlib
import os
import re
import socket
import subprocess
import sys
from pathlib import Path
from vivado_env import resolve_sdk_dir
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest().upper()
def parse_server(url: str):
match = re.fullmatch(r"tcp:([^:]+):(\d+)", url)
if not match:
raise SystemExit("ERROR: --hw-server-url must look like tcp:127.0.0.1:3121")
return match.group(1), int(match.group(2))
def main() -> None:
parser = argparse.ArgumentParser(description="Program and verify Zynq QSPI BOOT.bin")
parser.add_argument("--sdk-dir", help="Xilinx SDK root containing bin/program_flash.bat")
parser.add_argument("--image", required=True, help="BOOT.bin to program at offset 0")
parser.add_argument("--jtag-fsbl", required=True, help="helper FSBL loaded to OCM for programming")
parser.add_argument("--expect-sha256", required=True, help="exact expected BOOT.bin SHA-256")
parser.add_argument("--expect-size", type=int, help="exact expected BOOT.bin byte length")
parser.add_argument("--capacity-bytes", type=int, default=32 * 1024 * 1024)
parser.add_argument("--expect-jtag-fsbl-sha256", help="optional exact helper FSBL SHA-256")
parser.add_argument(
"--require-marker", action="append", default=[],
help="ASCII marker required in BOOT.bin; repeatable",
)
parser.add_argument(
"--flash-type",
choices=["qspi_single", "qspi_x1_single", "qspi_x2_single",
"qspi_dual_parallel", "qspi_dual_stacked",
"qspi_x1_dual_stacked", "qspi_x2_dual_stacked"],
default="qspi_dual_parallel",
)
parser.add_argument("--offset", default="0")
parser.add_argument("--hw-server-url", default="tcp:127.0.0.1:3121")
parser.add_argument("--log-file", help="write complete program_flash output here")
parser.add_argument("--dry-run", action="store_true", help="run guards and print command only")
args = parser.parse_args()
image = Path(args.image).expanduser().resolve()
jtag_fsbl = Path(args.jtag_fsbl).expanduser().resolve()
for label, path in (("BOOT.bin", image), ("JTAG helper FSBL", jtag_fsbl)):
if not path.is_file():
raise SystemExit(f"ERROR: missing {label}: {path}")
if args.capacity_bytes <= 0:
raise SystemExit("ERROR: --capacity-bytes must be positive")
if not re.fullmatch(r"[0-9A-Fa-f]{64}", args.expect_sha256):
raise SystemExit("ERROR: --expect-sha256 must contain exactly 64 hex digits")
if (args.expect_jtag_fsbl_sha256 and
not re.fullmatch(r"[0-9A-Fa-f]{64}", args.expect_jtag_fsbl_sha256)):
raise SystemExit(
"ERROR: --expect-jtag-fsbl-sha256 must contain exactly 64 hex digits"
)
try:
if int(args.offset, 0) != 0:
raise ValueError
except ValueError:
raise SystemExit("ERROR: Zynq BOOT.bin must be programmed at offset 0")
image_size = image.stat().st_size
image_hash = sha256(image)
expected_hash = args.expect_sha256.upper()
if image_hash != expected_hash:
raise SystemExit(f"ERROR: BOOT.bin SHA256 {image_hash} != expected {expected_hash}")
if args.expect_size is not None and image_size != args.expect_size:
raise SystemExit(
f"ERROR: BOOT.bin size {image_size} != expected {args.expect_size}"
)
if image_size >= args.capacity_bytes:
raise SystemExit(
f"ERROR: BOOT.bin size {image_size} is not below capacity {args.capacity_bytes}"
)
try:
marker_bytes = [(marker, marker.encode("ascii")) for marker in args.require_marker]
except UnicodeEncodeError:
raise SystemExit("ERROR: --require-marker values must be ASCII")
image_bytes = image.read_bytes()
missing = [marker for marker, raw in marker_bytes if raw not in image_bytes]
if missing:
raise SystemExit("ERROR: BOOT.bin lacks required markers: " + repr(missing))
helper_hash = sha256(jtag_fsbl)
if (args.expect_jtag_fsbl_sha256 and
helper_hash != args.expect_jtag_fsbl_sha256.upper()):
raise SystemExit(
f"ERROR: JTAG helper FSBL SHA256 {helper_hash} != expected "
f"{args.expect_jtag_fsbl_sha256.upper()}"
)
sdk_dir = Path(resolve_sdk_dir(args.sdk_dir)).expanduser().resolve()
program_flash = sdk_dir / "bin" / "program_flash.bat"
if not program_flash.is_file():
raise SystemExit(f"ERROR: program_flash not found: {program_flash}")
cmd = [
str(program_flash), "-f", str(image), "-offset", args.offset,
"-flash_type", args.flash_type, "-verify", "-fsbl", str(jtag_fsbl),
"-cable", "type", "xilinx_tcf", "url", args.hw_server_url,
]
print(f"[program_zynq_qspi] image : {image}")
print(f"[program_zynq_qspi] size/sha256 : {image_size} / {image_hash}")
print(f"[program_zynq_qspi] helper FSBL : {jtag_fsbl} / {helper_hash}")
print(f"[program_zynq_qspi] flash/server: {args.flash_type} / {args.hw_server_url}")
print("[program_zynq_qspi] command : " + subprocess.list2cmdline(cmd))
if args.dry_run:
print("DRY_RUN_OK: guards passed; hardware was not touched")
return
host, port = parse_server(args.hw_server_url)
try:
with socket.create_connection((host, port), timeout=3):
pass
except OSError as exc:
raise SystemExit(f"ERROR: hw_server is not reachable at {host}:{port}: {exc}")
result = subprocess.run(cmd, capture_output=True, text=True, errors="replace")
combined = (result.stdout or "") + (result.stderr or "")
sys.stdout.write(combined)
print(f"PROGRAM_FLASH_EXIT={result.returncode}")
if args.log_file:
log_path = Path(args.log_file).expanduser().resolve()
log_path.parent.mkdir(parents=True, exist_ok=True)
log_path.write_text(combined, encoding="utf-8", errors="replace")
print(f"PROGRAM_FLASH_LOG={log_path}")
required = [
"Erase Operation successful",
"Program Operation successful",
"Verify Operation successful",
"Flash Operation Successful",
]
missing_success = [marker for marker in required if marker not in combined]
explicit_failure = re.search(
r"(Verify Operation unsuccessful|Flash Operation Failed|Program Operation unsuccessful)",
combined, re.IGNORECASE,
)
if result.returncode != 0 or missing_success or explicit_failure:
raise SystemExit(
"ERROR: flash not proven successful; rc={} missing={} explicit_failure={}".format(
result.returncode, missing_success, bool(explicit_failure)
)
)
print("ZYNQ_QSPI_FLASH_VERIFY_PASS")
if __name__ == "__main__":
main()
+20
View File
@@ -56,3 +56,23 @@ def resolve_vivado_exe(cli_value=None):
if exe:
return exe
return os.path.join(resolve_vivado_dir(), "bin", "vivado.bat")
def resolve_sdk_dir(cli_value=None):
"""Xilinx SDK install ROOT (the dir containing bin/bootgen.bat).
Precedence matches the Vivado resolver: explicit CLI value, SDK_ROOT,
SDK_PATH, then the same keys in ~/skills_env.md.
"""
if cli_value:
return cli_value
root = env_lookup("SDK_ROOT")
if root:
return root
exe = env_lookup("SDK_PATH")
if exe:
return os.path.dirname(os.path.dirname(exe))
sys.exit(
"Xilinx SDK path not configured. Add `SDK_ROOT = <install root>` to "
"~/skills_env.md, set it as an env var, or pass --sdk-dir."
)
+20 -1
View File
@@ -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")
+12 -10
View File
@@ -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")
+53 -10
View File
@@ -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
+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.
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
View File
@@ -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))
+48 -1
View File
@@ -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)
+1 -1
View File
@@ -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`)——路径因机器而异,不要写死盘符 |
+4 -1
View File
@@ -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
+9
View File
@@ -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\SDK8.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())