Files
magnus919_agent-skills/fpga-development/scripts/fpga_fixture.py
T
Magnus HedemarkandGitHub f605e807f3 feat(fpga-development): add design and verification methodology (#512)
* feat(fpga-development): add design and verification methodology

* feat(fpga-development): integrate electronics verification routing
2026-09-14 18:37:01 -04:00

88 lines
4.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Run the vendor-neutral ready/valid FPGA fixture without touching hardware."""
import argparse
import hashlib
import json
import shutil
import subprocess
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ASSET = ROOT / "assets"
def clean(value):
if isinstance(value, bytes):
return value.decode(errors="replace")
return value or ""
def run(cmd, cwd, timeout=30):
try:
p = subprocess.run(cmd, cwd=cwd, text=True, capture_output=True, timeout=timeout)
return {"command": cmd, "returncode": p.returncode, "stdout": clean(p.stdout), "stderr": clean(p.stderr), "status": "ok" if p.returncode == 0 else "failed"}
except FileNotFoundError as e:
return {"command": cmd, "returncode": None, "error": str(e), "stdout": "", "stderr": "", "status": "missing_tool"}
except subprocess.TimeoutExpired as e:
return {"command": cmd, "returncode": None, "error": "timeout", "stdout": clean(e.stdout), "stderr": clean(e.stderr), "status": "timeout"}
def version(tool, flag="--version"):
if not tool:
return {"status": "missing_tool"}
return run([tool, flag], Path.cwd())
def source_hashes():
result = {}
for path in (ASSET / "rv_buffer.sv", ASSET / "tb_rv_buffer.sv"):
result[path.name] = hashlib.sha256(path.read_bytes()).hexdigest()
return result
def yq(path):
return '"' + str(path).replace('\\', '/') .replace('"', '\\"') + '"'
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--width", type=int, choices=(1, 8), nargs="+", default=[1, 8])
ap.add_argument("--seed", type=int, default=20260914)
ap.add_argument("--json", action="store_true")
ap.add_argument("--output", type=Path)
args = ap.parse_args()
out = (args.output.resolve() if args.output else Path(tempfile.mkdtemp(prefix="fpga-fixture-")))
if args.output and out.exists() and (not out.is_dir() or any(out.iterdir())):
ap.error(f"output directory must be new or empty: {out}")
out.mkdir(parents=True, exist_ok=True)
results = {"fixture": "rv_buffer", "hardware": False, "simulation": [], "synthesis": [], "output": str(out), "source_sha256": source_hashes()}
iverilog = shutil.which("iverilog")
vvp = shutil.which("vvp")
yosys = shutil.which("yosys")
for width in args.width:
sim = out / f"sim_w{width}.vvp"
if not iverilog or not vvp:
results["simulation"].append({"width": width, "status": "skipped", "reason": "iverilog or vvp unavailable"})
else:
sim_versions = {"iverilog": version(iverilog, "-V"), "vvp": version(vvp, "-V")}
compile_r = run([iverilog, "-g2012", "-s", "tb_rv_buffer", f"-Ptb_rv_buffer.DATA_WIDTH={width}", "-o", str(sim), str(ASSET / "rv_buffer.sv"), str(ASSET / "tb_rv_buffer.sv")], out)
if compile_r["returncode"] == 0:
exec_r = run([vvp, str(sim), f"+SEED={args.seed}"], out)
ok = exec_r["returncode"] == 0 and "PASS" in exec_r.get("stdout", "") and "ERROR" not in exec_r.get("stdout", "") and "FAIL" not in exec_r.get("stdout", "")
results["simulation"].append({"width": width, "status": "passed" if ok else "failed", "versions": sim_versions, "compile": compile_r, "run": exec_r})
else:
results["simulation"].append({"width": width, "status": "failed", "compile": compile_r})
if not yosys:
results["synthesis"].append({"width": width, "status": "skipped", "reason": "yosys unavailable"})
else:
ys = out / f"synth_w{width}.ys"
netlist = out / f"synth_w{width}.json"
ys.write_text(f"read_verilog -sv {yq(ASSET / 'rv_buffer.sv')}\nhierarchy -top rv_buffer -chparam DATA_WIDTH {width}\nproc; opt; check -assert; synth -top rv_buffer; check -assert; write_json {yq(netlist)}; stat\n")
syn = run([yosys, str(ys)], out)
results["synthesis"].append({"width": width, "status": "passed" if syn["returncode"] == 0 and netlist.exists() else "failed", "version": version(yosys), "run": syn, "netlist": str(netlist)})
statuses = [x["status"] for x in results["simulation"] + results["synthesis"]]
results["status"] = "passed" if statuses and all(x == "passed" for x in statuses) else ("incomplete" if statuses and all(x in ("passed", "skipped") for x in statuses) else "failed")
payload = json.dumps(results, indent=2)
if args.output:
(out / "result.json").write_text(payload + "\n")
print(payload if args.json else f"{results['status']}: simulation={[(x['width'], x['status']) for x in results['simulation']]} synthesis={[(x['width'], x['status']) for x in results['synthesis']]} output={out}")
return 0 if results["status"] == "passed" else 1
if __name__ == "__main__":
raise SystemExit(main())