Files
retroSoC/physical/librelane/tiny/scripts/doctor.py
T
maksyuki c8187284d0 feat(tiny): add scripts for SDC generation and packaging
- Introduced  to create pad-aware timing constraints for the Tiny IHP130 chip flow.
- Added  to create a checksummed delivery archive for the Tiny LibreLane implementation.
- Created  for synthesis-only header of the locked IHP SRAM macro physical views.
- Implemented  to limit OpenSTA report enumeration for path counts.
- Updated  to include power pad definitions.
- Enhanced  to handle power pad counts and validation.
- Added tests for Tiny flow and pin map validation, including power pad overrides and constraints.
2026-09-27 08:48:12 +08:00

166 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Validate the local LibreLane/IHP130 environment and Tiny chip configuration."""
from __future__ import annotations
import argparse
import json
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
sys.path.insert(0, str(ROOT))
from librelane.flows import Flow # noqa: E402
from scripts.rtl.generate_pin_map import read_power_pad_counts # noqa: E402
from scripts.setup_helpers import atomic_write # noqa: E402
EXPECTED_LIBRELANE_VERSION = "LibreLane v3.0.5"
EXPECTED_OPENSTA_VERSION = "3.0.0"
EXPECTED_SIGNAL_PADS = 52
def command_output(command: list[str]) -> str:
return subprocess.check_output(command, text=True, stderr=subprocess.STDOUT).strip()
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True)
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--lock", type=Path, required=True)
parser.add_argument("--pin-map", type=Path, required=True)
parser.add_argument("--pdk-root", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
errors: list[str] = []
details: dict[str, object] = {}
for tool in ("librelane", "klayout", "sta"):
path = shutil.which(tool)
details[f"{tool}_path"] = path
if path is None:
errors.append(f"required tool is missing: {tool}")
if shutil.which("librelane") is not None:
version = command_output(
[sys.executable, "-m", "librelane", "--version"]
).splitlines()[0]
details["librelane_version"] = version
if version != EXPECTED_LIBRELANE_VERSION:
errors.append(f"expected {EXPECTED_LIBRELANE_VERSION}, found {version or '<empty>'}")
if shutil.which("sta") is not None:
version = command_output(["sta", "-version"])
details["opensta_version"] = version
if version != EXPECTED_OPENSTA_VERSION:
errors.append(
f"expected OpenSTA {EXPECTED_OPENSTA_VERSION}, "
f"found {version or '<empty>'}"
)
lock = json.loads(args.lock.read_text(encoding="utf-8"))
expected_pdk_revision = lock["sources"]["pdk_ihp130"]["revision"]
details["locked_pdk_revision"] = expected_pdk_revision
pdk_checkout = args.pdk_root.resolve()
if not (pdk_checkout / ".git").exists():
errors.append(f"IHP PDK checkout is missing: {pdk_checkout}")
else:
revision = command_output(["git", "-C", str(pdk_checkout), "rev-parse", "HEAD"])
details["pdk_revision"] = revision
if revision != expected_pdk_revision:
errors.append(f"expected IHP PDK {expected_pdk_revision}, found {revision}")
required = (
args.config,
pdk_checkout / "ihp-sg13g2/libs.tech/librelane/config.tcl",
pdk_checkout / "ihp-sg13g2/libs.tech/klayout/tech/drc/ihp-sg13g2.drc",
pdk_checkout / "ihp-sg13g2/libs.tech/klayout/tech/scripts/sealring.py",
pdk_checkout / "ihp-sg13g2/libs.ref/sg13g2_io/spice/sg13g2_io.spice",
)
for path in required:
if not path.is_file():
errors.append(f"required LibreLane/IHP file is missing: {path}")
try:
power_counts = read_power_pad_counts(args.pin_map)
except (OSError, ValueError, json.JSONDecodeError) as error:
power_counts = None
errors.append(f"failed to read power PAD counts: {error}")
config: dict[str, object] = {}
flow_name = ""
if args.config.is_file():
try:
config = json.loads(args.config.read_text(encoding="utf-8"))
meta = config.get("meta", {})
if not isinstance(meta, dict) or not isinstance(meta.get("flow"), str):
raise ValueError("config meta.flow must name a LibreLane flow")
flow_name = meta["flow"]
except (OSError, ValueError, json.JSONDecodeError) as error:
errors.append(f"failed to read LibreLane configuration: {error}")
if args.config.is_file() and not errors:
try:
flow_class = Flow.factory.get(flow_name)
if flow_class is None:
raise ValueError(f"unknown LibreLane flow: {flow_name}")
flow = flow_class(
str(args.config.resolve()),
pdk="ihp-sg13g2",
pdk_root=str(pdk_checkout),
scl="sg13g2_stdcell",
pad="sg13g2_io",
design_dir=str(args.config.resolve().parent),
)
details["flow"] = flow_name
details["config_design"] = flow.config["DESIGN_NAME"]
details["drc_runset"] = str(flow.config["KLAYOUT_DRC_RUNSET"])
details["lvs_setup"] = str(flow.config["NETGEN_SETUP"])
details["pad_spice"] = [str(path) for path in flow.config["PAD_SPICE_MODELS"]]
except Exception as error: # LibreLane reports structured configuration exceptions.
errors.append(f"LibreLane {flow_name or 'unknown'} configuration failed to load: {error}")
if config:
if details.get("config_design") not in (None, "retrosoc_tiny_asic"):
errors.append(
f"Tiny chip flow expects DESIGN_NAME retrosoc_tiny_asic, "
f"found {details['config_design']}"
)
placed = [
item
for side in ("PAD_SOUTH", "PAD_EAST", "PAD_NORTH", "PAD_WEST")
for item in config.get(side, [])
]
details["placed_pad_count"] = len(placed)
if flow_name == "Chip" and power_counts is not None:
expected = EXPECTED_SIGNAL_PADS + sum(power_counts.values())
if len(placed) != expected or len(placed) != len(set(placed)):
errors.append(
f"expected {expected} unique placed signal/power PADs, found {len(placed)}"
)
elif flow_name != "Chip":
errors.append(f"Tiny supports only the Chip flow, found {flow_name}")
result = {
"schema_version": 1,
"status": "passed" if not errors else "failed",
"errors": errors,
"details": details,
}
atomic_write(args.output, json.dumps(result, indent=2, sort_keys=True) + "\n")
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1
print(f"LibreLane doctor passed: {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())