sync: SHANGHAI-U9 2026-07-27
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
---
|
||||
name: zynq-imx6-flow
|
||||
description: >
|
||||
Run the zynq-imx6-flow LangGraph workflow at E:\LangGraph_Design\zynq-imx6-flow —
|
||||
the closed-loop xmod (Zynq PS+PL + i.MX6) hardware-in-the-loop design/debug pipeline:
|
||||
cold power-cycle, connect JTAG, program bitstream/ELF or burn flash, restart the
|
||||
ps/imx6 app, analyze (ILA + PS/imx6 serial consoles), judge against design
|
||||
requirements, and iterate by modifying RTL / imx6 app / PS baremetal code. It drives
|
||||
the graph in-process (no langgraph dev) and hands the 6 interrupts (analyze /
|
||||
meets_requirement / propose / modify_fpga / modify_imx6 / modify_ps) back to this
|
||||
Claude session to execute. Also supports zynq-only boards (no i.MX6) and PS
|
||||
baremetal/petalinux modes.
|
||||
|
||||
ALWAYS use this skill whenever the user wants to run, start, drive, resume, or
|
||||
continue the zynq-imx6-flow workflow / xmod Zynq+imx6 design loop, or says things
|
||||
like "启动 zynq-imx6-flow", "跑 zynq 设计流程", "run the zynq FPGA flow", "执行 xmod
|
||||
设计任务", "继续 zynq 工作流", "resume the zynq design loop", even if they don't name
|
||||
the project path. Before driving, this skill confirms the langgraph conda env from
|
||||
/langgraph-vivado-install is in place.
|
||||
---
|
||||
|
||||
## What this skill does
|
||||
|
||||
Drives the **zynq-imx6-flow** graph (28 nodes / 39 flow-edges, 1:1 with
|
||||
`Notes/zynq-imx6-flow.excalidraw`, plan `Notes/Task00.md`). The deterministic
|
||||
hardware/check nodes (power, JTAG, program, restart) run in-process; the **6 interrupt
|
||||
nodes** pause and hand control to **this Claude session**, which does the real work
|
||||
(analysis, judgment, RTL/app/PS edits) and resumes the same thread. Persistence is a
|
||||
SqliteSaver file, so `start`/`resume` survive across separate process calls.
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Project root | 见 `~\skills_env.md` 的 `ZYNQ_IMX6_FLOW_ROOT`(driver 自动读取,也可 `--project-dir` 覆盖) |
|
||||
| Conda env | `langgraph` (path 见 `~\skills_env.md` 的 `CONDA_LANGGRAPH_PYTHON`;装环境用 `/langgraph-vivado-install`) |
|
||||
| Graph | `app.main_graph:build_graph` (in-process; no langgraph dev) |
|
||||
| Driver | `%USERPROFILE%\.claude\skills\zynq-imx6-flow\scripts\run_zynq_flow.py` |
|
||||
| Checkpoint | `<project>\runs\skill_checkpoint.sqlite` (keyed by `--thread-id`) |
|
||||
|
||||
**Drive the driver with the Bash tool (Git Bash), not PowerShell.** Windows PowerShell
|
||||
5.1 strips the double-quotes out of `--resume-json` payloads (see Gotchas), corrupting
|
||||
the JSON. Git Bash single-quoting passes it intact. All commands below are Bash and were
|
||||
run this way to verify them.
|
||||
|
||||
```bash
|
||||
PY="/c/Users/Administrator/miniconda3/envs/langgraph/python.exe"
|
||||
RUN="/c/Users/Administrator/.claude/skills/zynq-imx6-flow/scripts/run_zynq_flow.py"
|
||||
export PYTHONIOENCODING=utf-8
|
||||
```
|
||||
|
||||
## ⚠️ Hardware timing rule (power / USB-Blast)
|
||||
|
||||
**Before every power change to the board, USB-Blast must already be disconnected; only
|
||||
reconnect JTAG after power is up.** Never power the board up/down while USB-Blast is
|
||||
connected (protects the JTAG adapter and board). The graph topology already enforces
|
||||
this: `power_off → check_power_off →(是) blast_off_a → check_blast_off →(是) power_on →
|
||||
check_power →(是) blast_on → check_hw`. After `flash_burn` the graph returns to
|
||||
`power_off` for a full cold-restart before re-analyzing. Serial port numbers are **never
|
||||
hardcoded** — they come from `~/zynq_imx6_flow.conf` (see "Mock vs real").
|
||||
|
||||
## Step 0 — Confirm the environment (REQUIRED before driving)
|
||||
|
||||
The workflow runs in the `langgraph` conda env created by **`/langgraph-vivado-install`**.
|
||||
Verify it first so a missing env is a clear message, not a cryptic mid-run import error:
|
||||
|
||||
```bash
|
||||
"$PY" -c "from langgraph.checkpoint.sqlite import SqliteSaver; from langgraph.types import Command; import langgraph; print('[OK] langgraph')"
|
||||
```
|
||||
|
||||
- **`[OK]`** → proceed.
|
||||
- **import fails / no python** → tell the user to run **`/langgraph-vivado-install`**
|
||||
first (it creates the `langgraph` env with `langgraph` + `langgraph-checkpoint-sqlite`).
|
||||
Do not proceed until it succeeds.
|
||||
|
||||
Optional sanity check — the project's own acceptance suite (all mock, ~2 s):
|
||||
|
||||
```bash
|
||||
cd /e/LangGraph_Design/zynq-imx6-flow && "$PY" smoke_test.py # prints [PASS]
|
||||
```
|
||||
|
||||
## Step 1 — Start the workflow
|
||||
|
||||
Pick the board + first-pass program mode (defaults shown), then start on a fresh
|
||||
`--thread-id`:
|
||||
|
||||
```bash
|
||||
"$PY" "$RUN" start --thread-id zynq-<short-task-name> \
|
||||
--board-type xmod --ps-mode baremetal --program-mode bitstream --bit-path mock.bit
|
||||
```
|
||||
|
||||
- `--board-type` `xmod` (Zynq+i.MX6, default) | `zynq_only` (no i.MX6 — imx6 branch is skipped).
|
||||
- `--ps-mode` `baremetal` (default) | `petalinux`.
|
||||
- `--program-mode` `bitstream` (default, downloads bit → restart → analyze) |
|
||||
`debug_flash` (image already fused in flash → straight to analyze, no download).
|
||||
- `--bit-path` / `--elf-path` / `--flash-path` optional artifact paths.
|
||||
- Add `--real` only with hardware wired + board connected (default is MOCK — see below).
|
||||
|
||||
The driver prints **one JSON line** and exits:
|
||||
|
||||
| Exit | `status` | Meaning |
|
||||
|---|---|---|
|
||||
| 0 | `done` | Workflow finished (符合要求 / iteration cap) → Step 3 |
|
||||
| 1 | `error` | Read `error`, diagnose, report |
|
||||
| 2 | `interrupt` | An interrupt fired → read `interrupt.action` (and `pending_count`) → Step 2 |
|
||||
|
||||
## Step 2 — Handle the interrupt(s), then resume
|
||||
|
||||
On exit 2, `interrupt.action` (or the `pending_interrupts` list) tells you what to do.
|
||||
Perform it, then resume the **same `--thread-id`**. Repeat until exit 0.
|
||||
|
||||
| `action` | excalidraw node | What you (the session) do | Resume JSON |
|
||||
|---|---|---|---|
|
||||
| `analyze` | 通过ila/串口分析 | Grab ILA + the `sources` consoles (ps_console, and imx6_console if xmod); summarize | `{"analysis_log":"<摘要/路径>"}` |
|
||||
| `meets_requirement` | 符合设计要求? | Judge analysis vs the design requirement | `{"meets": true}` or `{"meets": false}` |
|
||||
| `propose` | 分析原因,提方案 | Diagnose; **review the plan with `/langgraph-plan-review`**; decide what to change + next program mode | `{"reason":"...","need_fpga":bool,"need_imx6":bool,"need_ps":bool,"program_mode":"bitstream"或"flash"}` |
|
||||
| `modify_fpga` | 改 RTL→综合→bit/flash | Edit RTL, run Vivado synth/impl, produce bit + flash | `{"bit_path":"...","flash_path":"..."}` |
|
||||
| `modify_imx6` | VM 改 app→交叉编译 | In the Ubuntu VM edit imx6 app + cross-compile | `{"app_path":"..."}` |
|
||||
| `modify_ps` | sdk 改 PS 裸机→ELF | In Xilinx SDK edit PS baremetal code + build (petalinux: rebuild app/image) | `{"elf_path":"..."}` |
|
||||
|
||||
**Single interrupt** — resume with the payload directly:
|
||||
|
||||
```bash
|
||||
"$PY" "$RUN" resume --thread-id zynq-<name> --resume-json '{"meets": false}'
|
||||
```
|
||||
|
||||
**Three-way parallel fan-out** — after a `propose` that sets `need_fpga`/`need_imx6`/
|
||||
`need_ps` true, `modify_fpga` + `modify_imx6` + `modify_ps` fire **in one superstep**
|
||||
(`pending_count: 3`). Resume them with **one map keyed by `action`** (subset allowed;
|
||||
unresumed ones stay pending and the deferred `round_done` still waits for every branch):
|
||||
|
||||
```bash
|
||||
"$PY" "$RUN" resume --thread-id zynq-<name> --resume-json \
|
||||
'{"modify_fpga":{"bit_path":"new.bit","flash_path":"new.bin"},"modify_imx6":{"app_path":"new-app"},"modify_ps":{"elf_path":"new.elf"}}'
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Always reuse the **same `--thread-id`** from `start` — that's how the sqlite checkpoint
|
||||
reattaches (different tasks → different thread ids → isolated state).
|
||||
- `propose` fans out to `decide_fpga`/`decide_imx6`/`decide_ps`; all-false goes straight
|
||||
to `round_done` → re-program. `board_type=zynq_only` forces `need_imx6=false`.
|
||||
- `program_mode` from `propose` chooses the re-program path after `round_done`:
|
||||
`bitstream` = download bit; `flash` = burn flash then full cold power/connect
|
||||
re-sequence (`flash_burn → power_off`). This is the big closed loop.
|
||||
- `"$PY" "$RUN" status --thread-id zynq-<name>` re-prints the current state without advancing.
|
||||
|
||||
## Step 3 — Finish
|
||||
|
||||
On exit 0 (`status: done`), report iterations completed and whether the design met
|
||||
requirements (`meets_requirement`) or hit the iteration cap. Offer to inspect
|
||||
`<project>\runs\` artifacts or the per-step `history`.
|
||||
|
||||
To abandon a paused run and start clean, delete `<project>\runs\skill_checkpoint.sqlite`
|
||||
or use a fresh `--thread-id`.
|
||||
|
||||
## Mock vs real
|
||||
|
||||
- **MOCK (default)** — sets `ZYNQ_IMX6_MOCK=1`; hardware/check nodes return success
|
||||
immediately. Dry-runs the *design-loop logic* and all 6 interrupts with no board. Ideal
|
||||
for demonstrating/validating the flow. Every command in this doc was verified in mock.
|
||||
- **Real (`--real`)** — sets `ZYNQ_IMX6_MOCK=0` and drives actual hardware. The `tools_hw`
|
||||
paths are wired (Modbus power relay `0x05`, USB-Blast `0xA0` frames over pyserial). Real
|
||||
mode additionally requires:
|
||||
1. **pyserial** in the env (`"$PY" -m pip install pyserial`) — not installed by default;
|
||||
without it `--real` returns `{"status":"error","error":"Real mode requires pyserial"}`.
|
||||
2. **`~/zynq_imx6_flow.conf`** with 4 sections `[power] [usb_blast] [imx6_console]
|
||||
[ps_console]` (template: project's `zynq_imx6_flow.example.conf`). Relay ports
|
||||
(`power`/`usb_blast`) may fall back to `~/vivado_mcp_usb.conf`. `zynq_only` boards
|
||||
don't need `[imx6_console]`. COM numbers differ per machine — find them in Device
|
||||
Manager → Ports (COM & LPT).
|
||||
3. A connected board + Vivado for the JTAG program/flash nodes.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **PowerShell 5.1 corrupts `--resume-json`.** `& $py $RUN resume --resume-json '{"meets": false}'`
|
||||
arrives at Python as `{meets: false}` — PS strips the double-quotes (and `\"`-escaping
|
||||
splits it on spaces instead). This is the known PSNativeCommandArgumentPassing bug,
|
||||
unfixed in 5.1. **Use the Bash tool.** Verified: the same JSON through Git Bash arrives
|
||||
intact (`analysis_log` values with spaces round-trip correctly).
|
||||
- **`round_done` is a deferred node** (`defer=True`). With the three-way fan-out it runs
|
||||
**exactly once** after all active modify/deploy branches finish (verified: iteration
|
||||
advances 0→1 once even with all three branches on). Don't expect it after only the
|
||||
first branch.
|
||||
- **`propose` must return `program_mode` ∈ {bitstream, flash}.** Any other value raises
|
||||
`propose returned invalid program_mode`. (The `start` `--program-mode` has a third
|
||||
option `debug_flash` for the first pass only; `propose` does not.)
|
||||
- **`langgraph.__version__` doesn't exist** on 1.2.9 — check the version via
|
||||
`importlib.metadata.version("langgraph")` if needed (smoke_test asserts `>= 0.2.39`).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Fix |
|
||||
|---|---|
|
||||
| `--resume-json` value shows up without quotes / split on spaces | You used PowerShell. Drive via the Bash tool (single-quoted JSON). |
|
||||
| `UnicodeEncodeError: 'gbk' ...` | Don't use bare `conda run`; use the env `python.exe` directly (the driver reconfigures stdout to UTF-8). |
|
||||
| `{"status":"error","error":"Real mode requires pyserial"}` | `--real` needs pyserial: `"$PY" -m pip install pyserial`. Or drop `--real` for a mock run. |
|
||||
| `SerialConfigError: Missing serial sections [...]` | Create `~/zynq_imx6_flow.conf` from `zynq_imx6_flow.example.conf` (relay ports may come from `~/vivado_mcp_usb.conf`). |
|
||||
| `import failed ... Run /langgraph-vivado-install` | The `langgraph` env is missing/incomplete → run `/langgraph-vivado-install`. |
|
||||
| `propose returned invalid program_mode` | The `propose` resume used something other than `bitstream`/`flash`. |
|
||||
| `resume` seems to start over | You passed a different `--thread-id` than `start`. Use the same one. |
|
||||
| Want to see the graph visually | Optional: `langgraph dev` via the project's `langgraph.json` opens Studio — not required to run. |
|
||||
@@ -0,0 +1,187 @@
|
||||
"""In-process driver for the zynq-imx6-flow LangGraph workflow.
|
||||
|
||||
Builds the compiled graph from the project's ``app`` package and drives it with a
|
||||
persistent SqliteSaver — no ``langgraph dev`` server. Mirrors the sibling
|
||||
fpga-imx6-flow driver: exit code 0 = done, 1 = error, 2 = interrupt fired.
|
||||
|
||||
Commands:
|
||||
start --project-dir <dir> [--program-mode bitstream|debug_flash]
|
||||
[--ps-mode baremetal|petalinux] [--board-type xmod|zynq_only]
|
||||
[--bit-path ..] [--elf-path ..] [--flash-path ..]
|
||||
[--thread-id ..] [--real] [--max-iterations N]
|
||||
resume --project-dir <dir> --resume-json '<json>' [--thread-id ..] [--real]
|
||||
status --project-dir <dir> [--thread-id ..]
|
||||
|
||||
Output: one JSON line on stdout describing status + the pending interrupt(s).
|
||||
|
||||
Resume payloads:
|
||||
* Single pending interrupt -> pass its payload directly, e.g. '{"meets": true}'.
|
||||
* Multiple pending (the three-way modify fan-out: modify_fpga + modify_imx6 +
|
||||
modify_ps can fire in one superstep) -> pass a map keyed by each interrupt's
|
||||
"action", e.g. '{"modify_fpga": {...}, "modify_imx6": {...}, "modify_ps": {...}}'.
|
||||
You may resume a subset; unresumed interrupts stay pending and round_done
|
||||
(defer=True) still waits for every active branch.
|
||||
|
||||
Run via the env python directly or ``conda run --no-capture-output`` to avoid the
|
||||
Windows GBK console re-encode crash on the Chinese interrupt payloads.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8") # emit UTF-8 even on a GBK console
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
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 zynq-imx6-flow checkout: ZYNQ_IMX6_FLOW_ROOT in ~/skills_env.md.
|
||||
DEFAULT_PROJECT = _env_lookup("ZYNQ_IMX6_FLOW_ROOT")
|
||||
|
||||
|
||||
def _emit(obj: dict, code: int) -> None:
|
||||
print(json.dumps(obj, ensure_ascii=False))
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def _pending(snap):
|
||||
return [intr for task in snap.tasks for intr in task.interrupts]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="Drive the zynq-imx6-flow graph in-process.")
|
||||
ap.add_argument("command", choices=["start", "resume", "status"])
|
||||
ap.add_argument("--project-dir", default=DEFAULT_PROJECT)
|
||||
ap.add_argument("--thread-id", default="zynq-skill-1")
|
||||
ap.add_argument("--resume-json", default="")
|
||||
ap.add_argument("--program-mode", default="bitstream",
|
||||
choices=["bitstream", "debug_flash"])
|
||||
ap.add_argument("--ps-mode", default="baremetal", choices=["baremetal", "petalinux"])
|
||||
ap.add_argument("--board-type", default="xmod", choices=["xmod", "zynq_only"])
|
||||
ap.add_argument("--bit-path", default="")
|
||||
ap.add_argument("--elf-path", default="")
|
||||
ap.add_argument("--flash-path", default="")
|
||||
ap.add_argument("--real", action="store_true",
|
||||
help="drive real hardware (tools_* must be wired; default is MOCK dry-run)")
|
||||
ap.add_argument("--max-iterations", type=int, default=8)
|
||||
args = ap.parse_args()
|
||||
|
||||
if not args.project_dir:
|
||||
_emit({"status": "error",
|
||||
"error": "zynq-imx6-flow 仓库路径未配置:在 ~/skills_env.md 增加 "
|
||||
"`ZYNQ_IMX6_FLOW_ROOT = <检出目录>`,或用 --project-dir 指定"}, 1)
|
||||
project = Path(args.project_dir)
|
||||
if not (project / "app" / "main_graph.py").exists():
|
||||
_emit({"status": "error",
|
||||
"error": f"app/main_graph.py not found under {project}; check --project-dir"}, 1)
|
||||
|
||||
# MOCK by default so the design loop can be dry-run with no hardware attached.
|
||||
os.environ["ZYNQ_IMX6_MOCK"] = "0" if args.real else "1"
|
||||
sys.path.insert(0, str(project))
|
||||
|
||||
try:
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.types import Command
|
||||
from app.config import initial_state
|
||||
from app.main_graph import build_graph
|
||||
except Exception as exc: # noqa: BLE001
|
||||
_emit({"status": "error",
|
||||
"error": f"import failed: {exc}. Run /langgraph-vivado-install to set up the env."}, 1)
|
||||
|
||||
runs = project / "runs"
|
||||
runs.mkdir(exist_ok=True)
|
||||
db = str(runs / "skill_checkpoint.sqlite")
|
||||
cfg = {"configurable": {"thread_id": args.thread_id}}
|
||||
|
||||
with SqliteSaver.from_conn_string(db) as cp:
|
||||
graph = build_graph(checkpointer=cp)
|
||||
try:
|
||||
if args.command == "start":
|
||||
init = initial_state(
|
||||
program_mode=args.program_mode,
|
||||
ps_mode=args.ps_mode,
|
||||
board_type=args.board_type,
|
||||
max_iterations=args.max_iterations,
|
||||
)
|
||||
if args.bit_path:
|
||||
init["bit_path"] = args.bit_path
|
||||
if args.elf_path:
|
||||
init["elf_path"] = args.elf_path
|
||||
if args.flash_path:
|
||||
init["flash_path"] = args.flash_path
|
||||
graph.invoke(init, cfg)
|
||||
elif args.command == "resume":
|
||||
payload = json.loads(args.resume_json) if args.resume_json else {}
|
||||
pend = _pending(graph.get_state(cfg))
|
||||
if len(pend) <= 1:
|
||||
graph.invoke(Command(resume=payload), cfg)
|
||||
else:
|
||||
# Multiple interrupts pending (three-way modify fan-out). payload
|
||||
# must be a map keyed by each interrupt's "action" (or its id).
|
||||
resume_map = {}
|
||||
for intr in pend:
|
||||
val = intr.value if isinstance(intr.value, dict) else {}
|
||||
action = val.get("action")
|
||||
if action in payload:
|
||||
resume_map[intr.id] = payload[action]
|
||||
elif intr.id in payload:
|
||||
resume_map[intr.id] = payload[intr.id]
|
||||
if not resume_map:
|
||||
_emit({"status": "error",
|
||||
"error": f"resume-json keys {list(payload)} matched none of the "
|
||||
f"pending actions {[i.value.get('action') for i in pend]}"}, 1)
|
||||
graph.invoke(Command(resume=resume_map), cfg)
|
||||
snap = graph.get_state(cfg)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
_emit({"status": "error", "error": str(exc)}, 1)
|
||||
|
||||
pend = _pending(snap)
|
||||
if snap.next:
|
||||
_emit({
|
||||
"status": "interrupt",
|
||||
"thread_id": args.thread_id,
|
||||
"mock": not args.real,
|
||||
"next": list(snap.next),
|
||||
"iteration": snap.values.get("iteration"),
|
||||
"interrupt": pend[0].value if pend else None,
|
||||
"pending_interrupts": [i.value for i in pend],
|
||||
"pending_count": len(pend),
|
||||
}, 2)
|
||||
else:
|
||||
_emit({
|
||||
"status": "done",
|
||||
"thread_id": args.thread_id,
|
||||
"iteration": snap.values.get("iteration"),
|
||||
"meets_requirement": snap.values.get("meets_requirement"),
|
||||
"steps": len(snap.values.get("history", [])),
|
||||
}, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user