sync: SHANGHAI-U9 2026-07-27

This commit is contained in:
bowei
2026-07-27 01:08:42 +08:00
parent 17632bea27
commit 8080703a0e
5 changed files with 1385 additions and 0 deletions
+199
View File
@@ -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. |
+187
View File
@@ -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()