From a25760faa7eab87253472a10a3a4e391e51ba045 Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Fri, 19 Jun 2026 00:42:03 +0530 Subject: [PATCH 01/17] sim.yml --- .github/workflows/sim.yml | 173 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 .github/workflows/sim.yml diff --git a/.github/workflows/sim.yml b/.github/workflows/sim.yml new file mode 100644 index 00000000..d8a8d460 --- /dev/null +++ b/.github/workflows/sim.yml @@ -0,0 +1,173 @@ +name: "Automated: Cell ngspice" + +# Triggered automatically when the DRC workflow finishes (success OR failure), +# in parallel with LVS. DRC produces the GDS + reference netlists, so the sim +# job just downloads the DRC artifact and runs ngspice on it — no need to +# rebuild the cells. +# +# Why hang off DRC (alongside LVS) instead of chaining off LVS: it reuses the +# exact trigger/artifact plumbing the LVS workflow already proves out, and +# keeps sim independent so a flaky LVS run doesn't block it. If you'd rather +# only simulate cells that passed LVS, point `workflows:` at +# "Automated: Cell LVS" and download the lvs-${{ matrix.pdk }} artifact below +# instead of drc-${{ matrix.pdk }}. +# +# Also runnable on demand against the latest DRC artifact. +on: + workflow_run: + workflows: ["Cell DRC"] + types: [completed] + workflow_dispatch: + inputs: + drc_run_id: + description: "GitHub Actions run id of the DRC workflow whose artifacts to consume (defaults to latest successful run)." + required: false + +# Same concurrency fallback as lvs.yml: workflow_run-triggered runs report +# github.ref as the default branch, so fall back to the triggering DRC run's +# head_branch when present. +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }} + cancel-in-progress: true + +jobs: + sim: + name: ngspice (${{ matrix.pdk }}) + runs-on: ubuntu-22.04 + timeout-minutes: 30 + # Skip only cancelled DRC runs; run sim on whatever cells DRC produced, + # mirroring lvs.yml. + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion != 'cancelled' }} + + # Same permission set as lvs.yml: actions:read for cross-run + # download-artifact, checks:write for the JUnit publisher. + permissions: + contents: read + actions: read + checks: write + + container: + image: hpretl/iic-osic-tools:latest + options: --user root + env: + PDK_ROOT: /foss/pdks + DEBIAN_FRONTEND: noninteractive + PYTHONUNBUFFERED: "1" + PYTHONPATH: "" + PATH: /foss/tools/bin:/foss/tools/sak:/foss/tools/kactus2:/foss/tools/klayout:/foss/tools/osic-multitool:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + + strategy: + fail-fast: false + matrix: + # Both PDKs ship ngspice model libs: sky130 at + # sky130A/libs.tech/ngspice/sky130.lib.spice, gf180 under the ciel + # versioned path. run_cell_sim.py resolves the right one per --pdk. + pdk: [sky130, gf180] + + defaults: + run: + shell: bash + + steps: + - uses: actions/checkout@v4 + + - name: Download DRC artifact (drc-${{ matrix.pdk }}) + uses: actions/download-artifact@v4 + with: + name: drc-${{ matrix.pdk }} + path: drc_inputs/${{ matrix.pdk }} + # From the triggering DRC run when via workflow_run; falls back to + # the manual input or current run for workflow_dispatch. + run-id: ${{ github.event.workflow_run.id || inputs.drc_run_id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Cache uv + CPython 3.10 + id: cache-uv + uses: actions/cache@v4 + with: + path: | + /headless/.local/bin/uv + /headless/.local/bin/uvx + /headless/.local/share/uv + key: uv-py310-${{ runner.os }}-v1 + + - name: Install Python 3.10 (uv) + run: | + set -euxo pipefail + if [ ! -x "$HOME/.local/bin/uv" ]; then + curl -LsSf https://astral.sh/uv/install.sh | sh + fi + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + export PATH="$HOME/.local/bin:$PATH" + uv python install 3.10 + echo "PYTHON310=$(uv python find 3.10)" >> "$GITHUB_ENV" + + - name: Show tool versions + run: | + set -euxo pipefail + ngspice --version 2>&1 | head -3 || true + "$PYTHON310" --version + ls "$PDK_ROOT" + # Surface the ngspice model library up front so a PDK install hiccup + # shows here rather than as a cryptic "can't find include" mid-sim. + if [ "${{ matrix.pdk }}" = "sky130" ]; then + ls "$PDK_ROOT/sky130A/libs.tech/ngspice/sky130.lib.spice" + else + ver=$(cat "$PDK_ROOT/ciel/gf180mcu/current") + ls "$PDK_ROOT/ciel/gf180mcu/versions/$ver/gf180mcuD/libs.tech/ngspice/" || true + fi + + - name: Cache python venv + id: cache-venv + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.venv + # Same key as drc.yml/lvs.yml: identical interpreter + deps, so all + # three workflows share a single restored venv. + key: drc-venv-py310-${{ runner.os }}-${{ hashFiles('setup.py', 'src/glayout/**/*.py') }}-v2 + restore-keys: | + drc-venv-py310-${{ runner.os }}- + + - name: Create venv and install glayout (cache miss) + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + set -euxo pipefail + rm -rf "$GITHUB_WORKSPACE/.venv" + "$PYTHON310" -m venv "$GITHUB_WORKSPACE/.venv" + . "$GITHUB_WORKSPACE/.venv/bin/activate" + uv pip install -e . + + # No "refresh editable install" step on cache hit — see drc.yml. + + - name: Sanity-check sim inputs + run: | + set -euxo pipefail + ls -la drc_inputs/${{ matrix.pdk }}/netlists || { echo "no netlists/ in DRC artifact"; exit 1; } + # gds/ is only needed if run_cell_sim.py does its own PEX extraction; + # a netlist-only (pre-layout) sim can proceed without it. + ls -la drc_inputs/${{ matrix.pdk }}/gds || echo "warning: no gds/ (ok for netlist-only sim)" + + - name: Run cell ngspice + run: | + set -euxo pipefail + . "$GITHUB_WORKSPACE/.venv/bin/activate" + python tests/sim/run_cell_sim.py \ + --pdk ${{ matrix.pdk }} \ + --inputs-dir drc_inputs/${{ matrix.pdk }} \ + --out-dir sim_results/${{ matrix.pdk }} + + - name: Upload sim artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: sim-${{ matrix.pdk }} + path: sim_results/${{ matrix.pdk }} + retention-days: 14 + + - name: Publish JUnit summary + if: ${{ always() && hashFiles(format('sim_results/{0}/junit.xml', matrix.pdk)) != '' }} + uses: mikepenz/action-junit-report@v4 + with: + report_paths: sim_results/${{ matrix.pdk }}/junit.xml + check_name: ngspice report (${{ matrix.pdk }}) + require_tests: true From bdf5fd2d84a98e3b66381071ffa77fb2c334fa82 Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Fri, 19 Jun 2026 00:46:49 +0530 Subject: [PATCH 02/17] Create run_cell_sim.py --- tests/sim/run_cell_sim.py | 399 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 399 insertions(+) create mode 100644 tests/sim/run_cell_sim.py diff --git a/tests/sim/run_cell_sim.py b/tests/sim/run_cell_sim.py new file mode 100644 index 00000000..d0524836 --- /dev/null +++ b/tests/sim/run_cell_sim.py @@ -0,0 +1,399 @@ +"""Runs ngspice on every cell using the reference netlist emitted by +``tests/drc/run_cell_drc.py`` plus a per-cell testbench. + +The sim CI workflow pulls the DRC artifact (``drc_results//``), which +contains: + + drc_results// + gds/.gds + netlists/.spice <-- DUT subckt, written by run_cell_drc.py + reports/.lyrdb + summary.json + +A cell is simulated when it has BOTH that reference netlist AND a testbench at: + + tests/sim/testbenches/.spice + +The testbench is the stimulus + analysis + `.measure` cards only. It must NOT +declare the model `.lib` or `.include` the DUT netlist — this runner injects +both so the same testbench works across corners/PDKs. Instantiate the DUT with +a subckt call whose name matches the `.subckt ` in the reference netlist, +e.g.: + + Vdd vdd 0 1.8 + Vin in 0 PULSE(0 1.8 1n 10p 10p 5n 10n) + X1 in out vdd 0 + .tran 10p 50n + .measure tran tphl TRIG v(in) VAL=0.9 RISE=1 TARG v(out) VAL=0.9 FALL=1 + +Optionally, a sidecar ``tests/sim/testbenches/.checks.json`` gives pass +bands for measurements; without it a cell passes when ngspice finishes with no +fatal error and no failed `.measure`: + + { "tphl": {"max": 2e-9}, "tplh": {"max": 2e-9} } + +This mirrors the LVS runner's shape so the same workflow plumbing (artifact +upload, JUnit publication) works unchanged. + +By default this simulates the *reference* netlist (pre-layout / functional +check). Post-layout (PEX) sim is a documented extension point in +``_assemble_deck`` — it's left off the default path because per-cell magic +extraction in CI is slow and, for gf180, mis-extracts the substrate (same +reason the LVS runner drives the klayout deck for gf180). +""" +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +import sys +import traceback +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + + +REPO_ROOT = Path(__file__).resolve().parents[2] + +# Per-PDK ngspice model library + default corner section. Resolved straight +# from PDK_ROOT rather than via a glayout pdk attribute, so this doesn't depend +# on an API the LVS path doesn't already use. Override per run with +# --model-lib / --corner if your install differs. +_DEFAULT_CORNER = {"sky130": "tt", "gf180": "typical"} + + +def _model_lib( + pdk_name: str, + path_override: Optional[str] = None, + corner_override: Optional[str] = None, +) -> Tuple[Path, str]: + corner = corner_override or _DEFAULT_CORNER[pdk_name] + if path_override: + return Path(path_override), corner + root = Path(os.environ.get("PDK_ROOT", "/foss/pdks")) + if pdk_name == "sky130": + return root / "sky130A/libs.tech/ngspice/sky130.lib.spice", corner + # gf180: ciel keeps the active version behind a `current` pointer (same + # pattern lvs.yml uses to locate the klayout deck). + ver = (root / "ciel/gf180mcu/current").read_text().strip() + base = root / "ciel/gf180mcu/versions" / ver / "gf180mcuD/libs.tech/ngspice" + cand = base / "design.ngspice" + if not cand.exists(): + libs = ( + sorted(base.glob("*.ngspice")) + + sorted(base.glob("*.lib.spice")) + + sorted(base.glob("*.spice")) + ) + if libs: + cand = libs[0] + return cand, corner + + +def _parse_sim_log(text: str, checks: Optional[Dict[str, dict]]) -> Dict[str, Any]: + """Lightweight parse of an ngspice batch log. + + Surfaces the common environment failures explicitly (mirroring the LVS + parser) so a broken container reads as e.g. "ngspice not on PATH" rather + than the catch-all "sim inconclusive". Then extracts `.measure` results and + decides pass/fail: clean finish + no failed measure + (if a checks sidecar + is given) every measurement inside its band. + """ + summary: Dict[str, Any] = { + "is_pass": False, + "conclusion": "sim inconclusive", + "measures": {}, + "failed_measures": [], + "check_violations": [], + "raw_tail": text[-1500:] if text else "", + } + if not text: + return summary + + if "ngspice: command not found" in text or "ngspice: not found" in text: + summary["conclusion"] = "ngspice binary not on PATH" + return summary + if "could not find include file" in text or "can't open file" in text.lower(): + summary["conclusion"] = "missing include / model lib" + return summary + # Model card not found / unresolved subckt — almost always a corner-section + # mismatch or a DUT subckt name that doesn't match the .subckt in the + # reference netlist. + if re.search(r"unable to find definition of model|could not find a model|unknown subckt|unknown subcircuit", text, re.I): + summary["conclusion"] = "missing model / unresolved subckt" + return summary + + fatal = re.search(r"\bfatal\b|singular matrix|Timestep too small|simulation (?:aborted|interrupted)|iteration limit reached", text, re.I) + + # Capture `name = 1.23e-9` style measurement echoes, and any that failed. + for m in re.finditer(r"^\s*([A-Za-z_]\w*)\s*=\s*(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)\b", text, re.M): + summary["measures"][m.group(1)] = float(m.group(2)) + for m in re.finditer(r"(?:measure(?:ment)?\s+)?([A-Za-z_]\w*)\s*(?:=\s*failed|\bfailed\b)", text, re.I): + if m.group(1).lower() not in ("the", "measurement"): + summary["failed_measures"].append(m.group(1)) + + if fatal: + summary["conclusion"] = f"ngspice error: {fatal.group(0).strip()}" + return summary + if summary["failed_measures"]: + summary["conclusion"] = "measurement failed" + return summary + + if checks: + for name, band in checks.items(): + if name not in summary["measures"]: + summary["check_violations"].append(f"{name}: not measured") + continue + val = summary["measures"][name] + lo, hi = band.get("min"), band.get("max") + if lo is not None and val < lo: + summary["check_violations"].append(f"{name}={val:g} < min {lo:g}") + if hi is not None and val > hi: + summary["check_violations"].append(f"{name}={val:g} > max {hi:g}") + if summary["check_violations"]: + summary["conclusion"] = "out-of-spec measurement" + return summary + + summary["is_pass"] = True + summary["conclusion"] = "sim passed" + return summary + + +def _write_junit(results: List[dict], pdk: str, out: Path) -> None: + suite = ET.Element( + "testsuite", + attrib={ + "name": f"glayout-sim-{pdk}", + "tests": str(len(results)), + "failures": str(sum(1 for r in results if r["status"] == "fail")), + "errors": str(sum(1 for r in results if r["status"] == "error")), + "skipped": str(sum(1 for r in results if r["status"] == "skip")), + }, + ) + for r in results: + case = ET.SubElement( + suite, "testcase", + attrib={"classname": f"sim.{pdk}", "name": r["cell"]}, + ) + if r["status"] == "fail": + ET.SubElement(case, "failure", attrib={"message": r.get("message", "sim failed")}).text = json.dumps(r, indent=2) + elif r["status"] == "error": + ET.SubElement(case, "error", attrib={"message": r.get("message", "sim error")}).text = json.dumps(r, indent=2) + elif r["status"] == "skip": + ET.SubElement(case, "skipped", attrib={"message": r.get("message", "skipped")}) + ET.ElementTree(suite).write(out, encoding="utf-8", xml_declaration=True) + + +def _enumerate_cells(inputs_dir: Path, tb_dir: Path) -> Tuple[List[str], int]: + """Cells that have BOTH a reference netlist and a testbench. + + Returns (simulatable_cells, total_netlists) so main() can tell the + difference between "broken artifact" (no netlists at all) and "nothing + wired up yet" (netlists exist but no testbenches). + """ + nets = {p.stem for p in (inputs_dir / "netlists").glob("*.spice")} if (inputs_dir / "netlists").is_dir() else set() + tbs = {p.stem for p in tb_dir.glob("*.spice")} if tb_dir.is_dir() else set() + return sorted(nets & tbs), len(nets) + + +def _assemble_deck(name: str, netlist_path: Path, testbench_path: Path, + model_lib: Path, corner: str, deck_path: Path) -> None: + """Write a self-contained ngspice deck: model lib + DUT netlist + testbench. + + POST-LAYOUT (PEX) EXTENSION: to simulate parasitics instead of the + reference netlist, extract a `.pex.spice` from gds/.gds via a + magic batch step (extract all; ext2spice cthresh 0; extresist all; + ext2spice extresist on) and point `.include` at that file instead of + `netlist_path`. Gate it behind a --pex flag and skip gf180 (substrate + mis-extraction, as in the LVS runner). + """ + body = testbench_path.read_text() + # Drop a trailing `.end` from the testbench so ours is the only one. + body = re.sub(r"^\s*\.end\s*$", "", body, flags=re.M | re.I).rstrip() + deck = ( + f"* auto-assembled deck for {name} ({corner})\n" + f'.lib "{model_lib}" {corner}\n' + f'.include "{netlist_path}"\n' + f"{body}\n" + f".end\n" + ) + deck_path.write_text(deck) + + +def _run_one_sim(item: dict) -> dict: + """Simulate one cell. Designed for ProcessPoolExecutor.""" + name = item["name"] + pdk_name = item["pdk"] + out_dir = Path(item["out_dir"]) + cell_dir = Path(item["rpt_dir"]) / "sim" / name + cell_dir.mkdir(parents=True, exist_ok=True) + deck_path = cell_dir / f"{name}.deck.spice" + log_path = cell_dir / f"{name}.log" + result: Dict[str, Any] = {"cell": name, "pdk": pdk_name, "status": "skip"} + + try: + print(f"[SIM] {name}", flush=True) + checks: Optional[Dict[str, dict]] = None + checks_path = Path(item["testbench_path"]).with_suffix(".checks.json") + if checks_path.exists(): + checks = json.loads(checks_path.read_text()) + + _assemble_deck( + name, + Path(item["netlist_path"]), + Path(item["testbench_path"]), + Path(item["model_lib"]), + item["corner"], + deck_path, + ) + + # Batch mode with NO `.control` block: avoids the well-known + # double-execution when `-b` is combined with `.control ... run`. + proc = subprocess.run( + ["ngspice", "-b", "-o", str(log_path), str(deck_path)], + capture_output=True, text=True, timeout=item["timeout"], + ) + captured = (proc.stdout or "") + "\n" + (proc.stderr or "") + log_text = (log_path.read_text() if log_path.exists() else "") + "\n" + captured + except subprocess.TimeoutExpired: + result.update({"status": "error", "message": f"sim timed out after {item['timeout']}s"}) + print(f"[ERROR] {name}: timed out", flush=True) + return result + except Exception as exc: + result.update({"status": "error", "message": f"sim failed: {exc}", "trace": traceback.format_exc()}) + print(f"[ERROR] {name}: {exc}", flush=True) + return result + + parsed = _parse_sim_log(log_text, checks) + result.update({ + "summary": parsed, + "returncode": proc.returncode, + "deck": str(deck_path.relative_to(out_dir)), + "log": str(log_path.relative_to(out_dir)) if log_path.exists() else None, + }) + if parsed["is_pass"] and proc.returncode == 0: + result["status"] = "pass" + result["message"] = "sim passed" + elif parsed["check_violations"]: + result["status"] = "fail" + result["message"] = "; ".join(parsed["check_violations"])[:300] + else: + result["status"] = "fail" + result["message"] = parsed["conclusion"] + print(f"[{result['status'].upper()}] {name}: {result.get('message','')}", flush=True) + return result + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--pdk", required=True, choices=["sky130", "gf180"]) + parser.add_argument( + "--inputs-dir", required=True, + help="Directory containing netlists/.spice (DRC artifact root for the PDK).", + ) + parser.add_argument("--out-dir", default="sim_results") + parser.add_argument( + "--testbench-dir", default=str(REPO_ROOT / "tests" / "sim" / "testbenches"), + help="Directory of .spice testbenches (and optional .checks.json).", + ) + parser.add_argument("--model-lib", default=None, help="Override ngspice model library path.") + parser.add_argument("--corner", default=None, help="Override corner section (default: sky130=tt, gf180=typical).") + parser.add_argument( + "--cells", default=None, + help="Comma-separated cell names; default runs every cell with both netlist+testbench.", + ) + parser.add_argument( + "--skip-cells", default="", + help="Comma-separated cell names to skip when --cells is not specified.", + ) + parser.add_argument("--timeout", type=int, default=600, help="Per-cell ngspice timeout in seconds.") + parser.add_argument( + "--jobs", "-j", type=int, default=max(1, (os.cpu_count() or 2) - 1), + help="Worker processes for parallel sims (default: cpu_count-1).", + ) + args = parser.parse_args() + + inputs_dir = Path(args.inputs_dir).resolve() + tb_dir = Path(args.testbench_dir).resolve() + out_dir = Path(args.out_dir).resolve() + rpt_dir = out_dir / "reports" + out_dir.mkdir(parents=True, exist_ok=True) + rpt_dir.mkdir(parents=True, exist_ok=True) + + cells, total_nets = _enumerate_cells(inputs_dir, tb_dir) + if total_nets == 0: + print(f"no netlists found under {inputs_dir}/netlists (broken DRC artifact?)", file=sys.stderr) + return 2 + if not cells: + # Netlists exist but nothing has a testbench yet. Treat as a no-op pass + # (write summary.json, no junit.xml) so adding this workflow doesn't red + # the build before testbenches are authored — the publish step is gated + # on junit.xml existing, so it just skips. + msg = f"no testbenches in {tb_dir} matching the {total_nets} available netlist(s); nothing to simulate" + print(msg) + (out_dir / "summary.json").write_text(json.dumps( + {"pdk": args.pdk, "total": 0, "note": msg}, indent=2)) + return 0 + + model_lib, corner = _model_lib(args.pdk, args.model_lib, args.corner) + print(f"model lib: {model_lib} (corner {corner})") + + if args.cells: + wanted = {c.strip() for c in args.cells.split(",") if c.strip()} + missing = wanted - set(cells) + if missing: + print(f"warning: cells without netlist+testbench: {sorted(missing)}", file=sys.stderr) + cells = [c for c in cells if c in wanted] + elif args.skip_cells: + skip = {c.strip() for c in args.skip_cells.split(",") if c.strip()} + for s in [c for c in cells if c in skip]: + print(f"skipping cell on the --skip-cells list: {s}") + cells = [c for c in cells if c not in skip] + + work_items = [ + { + "name": name, + "pdk": args.pdk, + "netlist_path": str(inputs_dir / "netlists" / f"{name}.spice"), + "testbench_path": str(tb_dir / f"{name}.spice"), + "model_lib": str(model_lib), + "corner": corner, + "out_dir": str(out_dir), + "rpt_dir": str(rpt_dir), + "timeout": args.timeout, + } + for name in cells + ] + jobs = max(1, min(args.jobs, len(work_items))) + print(f"running {len(work_items)} cells with {jobs} worker(s)") + from concurrent.futures import ProcessPoolExecutor, as_completed + results: List[dict] = [] + if jobs == 1: + for item in work_items: + results.append(_run_one_sim(item)) + else: + with ProcessPoolExecutor(max_workers=jobs) as pool: + futures = {pool.submit(_run_one_sim, item): item["name"] for item in work_items} + for fut in as_completed(futures): + results.append(fut.result()) + name_order = {n: i for i, n in enumerate(cells)} + results.sort(key=lambda r: name_order.get(r["cell"], len(name_order))) + + summary = { + "pdk": args.pdk, + "total": len(results), + "pass": sum(1 for r in results if r["status"] == "pass"), + "fail": sum(1 for r in results if r["status"] == "fail"), + "error": sum(1 for r in results if r["status"] == "error"), + "skip": sum(1 for r in results if r["status"] == "skip"), + "results": results, + } + (out_dir / "summary.json").write_text(json.dumps(summary, indent=2)) + _write_junit(results, args.pdk, out_dir / "junit.xml") + print(json.dumps({k: v for k, v in summary.items() if k != "results"}, indent=2)) + return 0 if summary["fail"] == 0 and summary["error"] == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) From 7205a1803177cd09767aedc11ecbf9444b9ddb57 Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Fri, 19 Jun 2026 00:49:10 +0530 Subject: [PATCH 03/17] Create current_mirror_nfet.spice --- .../sim/testbenches/current_mirror_nfet.spice | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/sim/testbenches/current_mirror_nfet.spice diff --git a/tests/sim/testbenches/current_mirror_nfet.spice b/tests/sim/testbenches/current_mirror_nfet.spice new file mode 100644 index 00000000..da3a0bf7 --- /dev/null +++ b/tests/sim/testbenches/current_mirror_nfet.spice @@ -0,0 +1,32 @@ +* Testbench: NFET current mirror (CMIRROR ports: VREF VOUT VSS B) +* +* Functional check: the output branch mirrors a 10 uA reference 1:1. +* +* run_cell_sim.py injects the .lib model line and .includes the DUT netlist, +* so this file is stimulus + analysis ONLY (no .lib / no .include here). +* +* Cross-PDK note: bias uses a 0.9 V output operating point well inside +* saturation for a 10 uA device on BOTH sky130 (1.8 V) and gf180 (3.3 V) +* parts. The mirror *ratio* is what we test, and that's process-independent +* as long as both transistors sit in saturation -- so one deck serves both. + +.param IREF=10u + +* Reference branch: push IREF into the diode-connected VREF node, down to VSS(0). +Iref 0 VREF DC {IREF} + +* Output branch: bias VOUT through a 0 V ammeter so i(vmeas) is the mirror current. +Vbias vom 0 DC 0.9 +Vmeas vom VOUT DC 0 + +* DUT: shared source (VSS) and bulk (B) tied to ground for the nfet mirror. +X1 VREF VOUT 0 0 CMIRROR + +* Sweep the output node 0 -> 1.8 V (triode through saturation). +.dc Vbias 0 1.8 0.02 + +.measure dc iout_op FIND i(vmeas) AT=0.9 +.measure dc iout_hi FIND i(vmeas) AT=1.6 +.measure dc mirror_err param='(iout_op-10e-6)/10e-6' + +.end From af00a1f5175da16eb034b4da48a155ff6fecfd2f Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Fri, 19 Jun 2026 00:49:51 +0530 Subject: [PATCH 04/17] Create current_mirror_pfet.spice --- .../sim/testbenches/current_mirror_pfet.spice | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/sim/testbenches/current_mirror_pfet.spice diff --git a/tests/sim/testbenches/current_mirror_pfet.spice b/tests/sim/testbenches/current_mirror_pfet.spice new file mode 100644 index 00000000..3e849c19 --- /dev/null +++ b/tests/sim/testbenches/current_mirror_pfet.spice @@ -0,0 +1,34 @@ +* Testbench: PFET current mirror (CMIRROR ports: VREF VOUT VSS B) +* +* Functional check: the output branch mirrors a 10 uA reference 1:1. +* +* run_cell_sim.py injects the .lib model line and .includes the DUT netlist, +* so this file is stimulus + analysis ONLY (no .lib / no .include here). +* +* For the pfet mirror the "VSS" net is the common SOURCE and ties to the top +* rail, as does the bulk B. Rail held at 1.8 V so the deck is valid on both +* sky130 and gf180 (the gf180 3.3 V part just runs under-driven, still in +* saturation at the 0.9 V output operating point). + +.param IREF=10u + +Vdd VDD 0 DC 1.8 + +* Reference branch: pull IREF out of the diode-connected VREF node. +Iref VREF 0 DC {IREF} + +* Output branch: 0 V ammeter oriented so sourced current reads positive. +Vbias vom 0 DC 0.9 +Vmeas VOUT vom DC 0 + +* DUT: source (VSS net) and bulk (B) tied to VDD for the pfet mirror. +X1 VREF VOUT VDD VDD CMIRROR + +* Sweep the output node 0 -> 1.8 V (saturation through triode). +.dc Vbias 0 1.8 0.02 + +.measure dc iout_op FIND i(vmeas) AT=0.9 +.measure dc iout_lo FIND i(vmeas) AT=0.2 +.measure dc mirror_err param='(iout_op-10e-6)/10e-6' + +.end From 4a0c5b055e9c69146ce1c392371ca1a2b3cac14f Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Thu, 25 Jun 2026 22:24:07 +0530 Subject: [PATCH 05/17] Update sim.yml --- .github/workflows/sim.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sim.yml b/.github/workflows/sim.yml index d8a8d460..c446a75b 100644 --- a/.github/workflows/sim.yml +++ b/.github/workflows/sim.yml @@ -62,7 +62,7 @@ jobs: # Both PDKs ship ngspice model libs: sky130 at # sky130A/libs.tech/ngspice/sky130.lib.spice, gf180 under the ciel # versioned path. run_cell_sim.py resolves the right one per --pdk. - pdk: [sky130, gf180] + pdk: [sky130] defaults: run: From ed45c283f19cbfdf6305caacd8fe1cfee2b93c70 Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:03:20 +0530 Subject: [PATCH 06/17] Create transmission_gate.spice --- tests/sim/testbenches/transmission_gate.spice | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/sim/testbenches/transmission_gate.spice diff --git a/tests/sim/testbenches/transmission_gate.spice b/tests/sim/testbenches/transmission_gate.spice new file mode 100644 index 00000000..21cf6f3e --- /dev/null +++ b/tests/sim/testbenches/transmission_gate.spice @@ -0,0 +1,38 @@ +* Testbench: CMOS transmission gate +* Transmission_Gate ports: VIN VSS VOUT VCC VGP VGN +* +* Functional check: with the gate ENABLED (VGN high, VGP low) the switch +* conducts from VIN to VOUT with a low, roughly rail-flat on-resistance. We +* pull a fixed load current out of VOUT and measure the IR drop across the +* closed switch at low / mid / high common mode; Ron = (V(VIN)-V(VOUT))/ILOAD. +* The complementary nfet+pfet keep Ron bounded across the whole input range -- +* that flatness is the TG's defining property and what we test here. +* +* run_cell_sim.py injects the .lib model line and .includes the DUT netlist, +* so this file is stimulus + analysis ONLY (no .lib / no .include here). +* +* sky130 (1.8 V) rails. To retarget gf180, bump VDD to 3.3 and move the AT= +* probe points / ILOAD accordingly. +.param VDD=1.8 +.param ILOAD=10u +* Rails feeding the DUT (nfet bulk = VSS, pfet bulk = VCC inside the subckt). +Vcc VCC 0 DC {VDD} +Vss VSS 0 DC 0 +* Gate drive: switch ON -> nmos gate high, pmos gate low. +Vgn VGN 0 DC {VDD} +Vgp VGP 0 DC 0 +* Swept input we pass through the closed switch. +Vin VIN 0 DC 0 +* Load: sink ILOAD out of VOUT so the closed switch must conduct it. +Iload VOUT 0 DC {ILOAD} +* DUT: VIN VSS VOUT VCC VGP VGN +X1 VIN VSS VOUT VCC VGP VGN Transmission_Gate +.dc Vin 0 {VDD} 0.02 +.measure dc vout_lo FIND v(VOUT) AT=0.3 +.measure dc vout_mid FIND v(VOUT) AT=0.9 +.measure dc vout_hi FIND v(VOUT) AT=1.5 +* Ron at each common mode. Literal 10e-6 must match ILOAD above. +.measure dc ron_lo param='(0.3 - vout_lo)/10e-6' +.measure dc ron_mid param='(0.9 - vout_mid)/10e-6' +.measure dc ron_hi param='(1.5 - vout_hi)/10e-6' +.end From 9d6721dd72372b034f828f4588dabb5021fc0e1a Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:04:52 +0530 Subject: [PATCH 07/17] Create diff_pair.spice --- tests/sim/testbenches/diff_pair.spice | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/sim/testbenches/diff_pair.spice diff --git a/tests/sim/testbenches/diff_pair.spice b/tests/sim/testbenches/diff_pair.spice new file mode 100644 index 00000000..c3e68183 --- /dev/null +++ b/tests/sim/testbenches/diff_pair.spice @@ -0,0 +1,33 @@ +* Testbench: NMOS differential pair +* DIFF_PAIR ports: VP VN VDD1 VDD2 VTAIL B +* (VDD1/VDD2 are the drain output nodes; VTAIL is the common source.) +* +* Functional check: a tail current sunk from VTAIL steers between the two +* drain branches according to Vid = VP - VN. At VP = VN the pair is balanced +* (each branch carries ITAIL/2); the balance error and the recovered tail +* current are what we test. Matching is process-independent, mirroring the +* "ratio is what we test" idea in the current-mirror deck. +* +* run_cell_sim.py injects the .lib + DUT .include; stimulus + analysis ONLY. +* sky130 (1.8 V). One deck serves both the w=3 and w=5 DIFF_PAIR variants. +.param VDD=1.8 +.param ITAIL=20u +.param VCM=0.9 +Vb B 0 DC 0 +* Drains pulled up to VDD through 0 V ammeters -> i(vmeasX) is branch current. +Vdd DD 0 DC {VDD} +Vmeas1 DD VDD1 DC 0 +Vmeas2 DD VDD2 DC 0 +* Tail current sink (sources of the pair return through here to ground). +Itail VTAIL 0 DC {ITAIL} +* Inputs: VN held at the common mode, VP swept through it. +Vn VN 0 DC {VCM} +Vp VP 0 DC 0 +* DUT: VP VN VDD1 VDD2 VTAIL B +X1 VP VN VDD1 VDD2 VTAIL B DIFF_PAIR +.dc Vp 0.6 1.2 0.01 +.measure dc i1_bal FIND i(vmeas1) AT={VCM} +.measure dc i2_bal FIND i(vmeas2) AT={VCM} +.measure dc itail_rec param='i1_bal + i2_bal' +.measure dc imbalance param='(i1_bal - i2_bal)/(i1_bal + i2_bal)' +.end From 4600a02aed1659657df14ea4f8e4a5dabd66488d Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:05:50 +0530 Subject: [PATCH 08/17] Create FVF.spice --- tests/sim/testbenches/FVF.spice | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/sim/testbenches/FVF.spice diff --git a/tests/sim/testbenches/FVF.spice b/tests/sim/testbenches/FVF.spice new file mode 100644 index 00000000..098a26a0 --- /dev/null +++ b/tests/sim/testbenches/FVF.spice @@ -0,0 +1,27 @@ +* Testbench: flipped voltage follower (FVF) +* FLIPPED_VOLTAGE_FOLLOWER ports: VIN VBULK VOUT Ib +* Internally: M0 gate=VIN, source=VOUT, drain=Ib; M1 gate=Ib, drain=VOUT, +* source=VBULK. Bias current is injected into the Ib node. +* +* Functional check: VOUT follows VIN with a roughly constant level shift +* (VOUT ~ VIN - Vgs0) -- so over the input range the small-signal gain +* (slope dVOUT/dVIN) should be near 1. We measure that slope plus the DC +* level shift. (Low output impedance is the other FVF virtue; left as an +* extension -- add a load-current step on VOUT and measure dVOUT.) +* +* run_cell_sim.py injects the .lib + DUT .include; stimulus + analysis ONLY. +* sky130 (1.8 V). Same deck covers FLIPPED_VOLTAGE_FOLLOWER_1 (identical ports). +.param VDD=1.8 +.param IB=10u +Vbulk VBULK 0 DC 0 +Vin VIN 0 DC 0 +* Inject the bias current into the Ib node (M0 drain / M1 gate). +Ibias 0 Ib DC {IB} +* DUT: VIN VBULK VOUT Ib +X1 VIN VBULK VOUT Ib FLIPPED_VOLTAGE_FOLLOWER +.dc Vin 0.6 {VDD} 0.02 +.measure dc vout_a FIND v(VOUT) AT=0.9 +.measure dc vout_b FIND v(VOUT) AT=1.2 +.measure dc level_shift param='0.9 - vout_a' +.measure dc follow_slope param='(vout_b - vout_a)/0.3' +.end From 86ee45bb18435f8bad467c79365821ab61a452c8 Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:07:01 +0530 Subject: [PATCH 09/17] Create low_voltage_current_mirror.spice --- .../low_voltage_current_mirror.spice | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/sim/testbenches/low_voltage_current_mirror.spice diff --git a/tests/sim/testbenches/low_voltage_current_mirror.spice b/tests/sim/testbenches/low_voltage_current_mirror.spice new file mode 100644 index 00000000..aceb2d13 --- /dev/null +++ b/tests/sim/testbenches/low_voltage_current_mirror.spice @@ -0,0 +1,30 @@ +* Testbench: low-voltage cascode current mirror +* Low_voltage_current_mirror ports: IBIAS1 IBIAS2 GND IOUT1 IOUT2 +* IBIAS1 is the reference input node; IBIAS2 is an internally-generated +* second-tier bias (left to float to its own operating point); IOUT1/IOUT2 +* are the two mirrored output branches. +* +* Functional check: push a reference current into IBIAS1 and confirm both +* output branches sink ~the same current (1:1 mirror) when held at mid-rail. +* Swept reference so the ratio is checked across a range, not one point. +* +* run_cell_sim.py injects the .lib + DUT .include; stimulus + analysis ONLY. +* sky130 (1.8 V). NOTE: this cell wraps the FVF feedback loops, so the bias +* point is delicate -- calibrate the check bands against a golden ngspice run. +.param VDD=1.8 +.param IREF=10u +Vgnd GND 0 DC 0 +* Reference current into the IBIAS1 node. +Iref 0 IBIAS1 DC {IREF} +* Outputs held near mid-rail through ammeters; i(vmX) is the sunk mirror current. +Vsup DD 0 DC 0.9 +Vm1 DD IOUT1 DC 0 +Vm2 DD IOUT2 DC 0 +* DUT: IBIAS1 IBIAS2 GND IOUT1 IOUT2 (IBIAS2 left on its own net) +X1 IBIAS1 net_ibias2 GND IOUT1 IOUT2 Low_voltage_current_mirror +.dc Iref 2u 20u 1u +.measure dc iout1_op FIND i(vm1) AT=10u +.measure dc iout2_op FIND i(vm2) AT=10u +.measure dc err1 param='(iout1_op - 10e-6)/10e-6' +.measure dc err2 param='(iout2_op - 10e-6)/10e-6' +.end From 5056ba5525b475c52389900686bc7e3268e373ba Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:07:47 +0530 Subject: [PATCH 10/17] Create diff_pair_cmirror.spice --- tests/sim/testbenches/diff_pair_cmirror.spice | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/sim/testbenches/diff_pair_cmirror.spice diff --git a/tests/sim/testbenches/diff_pair_cmirror.spice b/tests/sim/testbenches/diff_pair_cmirror.spice new file mode 100644 index 00000000..3238b8bc --- /dev/null +++ b/tests/sim/testbenches/diff_pair_cmirror.spice @@ -0,0 +1,34 @@ +* Testbench: differential pair with current-mirror tail bias +* DIFFPAIR_CMIRROR_BIAS ports: VP VN VDD1 VDD2 IBIAS VSS B +* Internally the diff-pair tail (wire0) is the output of a 1:1 mirror whose +* reference is IBIAS, so the tail current = mirror(IBIAS). +* +* Functional check: set the reference current at IBIAS, then verify (a) the +* tail current recovered at the drains tracks IBIAS, and (b) the pair is +* balanced at VP = VN. Combines the mirror and diff-pair checks into the +* assembled block. +* +* run_cell_sim.py injects the .lib + DUT .include; stimulus + analysis ONLY. +* sky130 (1.8 V). +.param VDD=1.8 +.param IBIAS=20u +.param VCM=0.9 +Vss VSS 0 DC 0 +Vb B 0 DC 0 +* Reference current into the mirror's diode node. +Iref 0 IBIAS DC {IBIAS} +* Diff-pair drains to VDD through ammeters. +Vdd DD 0 DC {VDD} +Vmeas1 DD VDD1 DC 0 +Vmeas2 DD VDD2 DC 0 +* Inputs: VN at common mode, VP swept through it. +Vn VN 0 DC {VCM} +Vp VP 0 DC 0 +* DUT: VP VN VDD1 VDD2 IBIAS VSS B +X1 VP VN VDD1 VDD2 IBIAS VSS B DIFFPAIR_CMIRROR_BIAS +.dc Vp 0.6 1.2 0.01 +.measure dc i1_bal FIND i(vmeas1) AT={VCM} +.measure dc i2_bal FIND i(vmeas2) AT={VCM} +.measure dc itail_rec param='i1_bal + i2_bal' +.measure dc imbalance param='(i1_bal - i2_bal)/(i1_bal + i2_bal)' +.end From 04164017f5c5e272611aba6286c77b8824ea6179 Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:09:42 +0530 Subject: [PATCH 11/17] Update FVF.spice --- tests/sim/testbenches/FVF.spice | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sim/testbenches/FVF.spice b/tests/sim/testbenches/FVF.spice index 098a26a0..b45a21ff 100644 --- a/tests/sim/testbenches/FVF.spice +++ b/tests/sim/testbenches/FVF.spice @@ -5,7 +5,7 @@ * * Functional check: VOUT follows VIN with a roughly constant level shift * (VOUT ~ VIN - Vgs0) -- so over the input range the small-signal gain -* (slope dVOUT/dVIN) should be near 1. We measure that slope plus the DC +* (slope dVOUT/dVIN) should be near 1. Measure that slope plus the DC * level shift. (Low output impedance is the other FVF virtue; left as an * extension -- add a load-current step on VOUT and measure dVOUT.) * From c964d64d3882a8a8c12c8b7fa0c7eabbd20d7870 Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:11:35 +0530 Subject: [PATCH 12/17] Update sim.yml --- .github/workflows/sim.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/sim.yml b/.github/workflows/sim.yml index c446a75b..6d56b511 100644 --- a/.github/workflows/sim.yml +++ b/.github/workflows/sim.yml @@ -7,10 +7,7 @@ name: "Automated: Cell ngspice" # # Why hang off DRC (alongside LVS) instead of chaining off LVS: it reuses the # exact trigger/artifact plumbing the LVS workflow already proves out, and -# keeps sim independent so a flaky LVS run doesn't block it. If you'd rather -# only simulate cells that passed LVS, point `workflows:` at -# "Automated: Cell LVS" and download the lvs-${{ matrix.pdk }} artifact below -# instead of drc-${{ matrix.pdk }}. +# keeps sim independent so a flaky LVS run doesn't block it. # # Also runnable on demand against the latest DRC artifact. on: From f643dbfc53829f714750dcb778fcedfe56122b18 Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:15:46 +0530 Subject: [PATCH 13/17] Rename FVF.spice to flipped_voltage_follower.spice --- .../sim/testbenches/{FVF.spice => flipped_voltage_follower.spice} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/sim/testbenches/{FVF.spice => flipped_voltage_follower.spice} (100%) diff --git a/tests/sim/testbenches/FVF.spice b/tests/sim/testbenches/flipped_voltage_follower.spice similarity index 100% rename from tests/sim/testbenches/FVF.spice rename to tests/sim/testbenches/flipped_voltage_follower.spice From 5994ee4918126f8198ae75dd2bfecbd18bc61dbb Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:16:26 +0530 Subject: [PATCH 14/17] Rename low_voltage_current_mirror.spice to low_voltage_cmirror.spice --- ...low_voltage_current_mirror.spice => low_voltage_cmirror.spice} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/sim/testbenches/{low_voltage_current_mirror.spice => low_voltage_cmirror.spice} (100%) diff --git a/tests/sim/testbenches/low_voltage_current_mirror.spice b/tests/sim/testbenches/low_voltage_cmirror.spice similarity index 100% rename from tests/sim/testbenches/low_voltage_current_mirror.spice rename to tests/sim/testbenches/low_voltage_cmirror.spice From 3a4b4efa54dde655dc98d9037d8335fc38967de4 Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:17:14 +0530 Subject: [PATCH 15/17] Rename diff_pair_cmirror.spice to diff_pair_ibias.spice --- .../{diff_pair_cmirror.spice => diff_pair_ibias.spice} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/sim/testbenches/{diff_pair_cmirror.spice => diff_pair_ibias.spice} (100%) diff --git a/tests/sim/testbenches/diff_pair_cmirror.spice b/tests/sim/testbenches/diff_pair_ibias.spice similarity index 100% rename from tests/sim/testbenches/diff_pair_cmirror.spice rename to tests/sim/testbenches/diff_pair_ibias.spice From 45d6c730c8f124a42bd65be8912bdcf8af9accca Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Fri, 26 Jun 2026 01:13:42 +0530 Subject: [PATCH 16/17] Update low_voltage_cmirror.spice --- tests/sim/testbenches/low_voltage_cmirror.spice | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/sim/testbenches/low_voltage_cmirror.spice b/tests/sim/testbenches/low_voltage_cmirror.spice index aceb2d13..75e4f606 100644 --- a/tests/sim/testbenches/low_voltage_cmirror.spice +++ b/tests/sim/testbenches/low_voltage_cmirror.spice @@ -1,5 +1,5 @@ * Testbench: low-voltage cascode current mirror -* Low_voltage_current_mirror ports: IBIAS1 IBIAS2 GND IOUT1 IOUT2 +* low_voltage_cmirror ports: IBIAS1 IBIAS2 GND IOUT1 IOUT2 * IBIAS1 is the reference input node; IBIAS2 is an internally-generated * second-tier bias (left to float to its own operating point); IOUT1/IOUT2 * are the two mirrored output branches. @@ -13,15 +13,17 @@ * point is delicate -- calibrate the check bands against a golden ngspice run. .param VDD=1.8 .param IREF=10u -Vgnd GND 0 DC 0 * Reference current into the IBIAS1 node. Iref 0 IBIAS1 DC {IREF} * Outputs held near mid-rail through ammeters; i(vmX) is the sunk mirror current. Vsup DD 0 DC 0.9 Vm1 DD IOUT1 DC 0 Vm2 DD IOUT2 DC 0 -* DUT: IBIAS1 IBIAS2 GND IOUT1 IOUT2 (IBIAS2 left on its own net) -X1 IBIAS1 net_ibias2 GND IOUT1 IOUT2 Low_voltage_current_mirror +* DUT: IBIAS1 IBIAS2 GND IOUT1 IOUT2 +* GND port tied straight to node 0 (no source -- a 0 V source here both ends +* on ground reads as a shorted VSRC and aborts the analysis). IBIAS2 left on +* its own net. +X1 IBIAS1 net_ibias2 0 IOUT1 IOUT2 low_voltage_cmirror .dc Iref 2u 20u 1u .measure dc iout1_op FIND i(vm1) AT=10u .measure dc iout2_op FIND i(vm2) AT=10u From 03d6be83e827d72520d1a9af379707f5250d7578 Mon Sep 17 00:00:00 2001 From: Nimish Kapoor <67710754+Nimok15@users.noreply.github.com> Date: Fri, 26 Jun 2026 02:07:40 +0530 Subject: [PATCH 17/17] Update low_voltage_cmirror.spice --- .../sim/testbenches/low_voltage_cmirror.spice | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/sim/testbenches/low_voltage_cmirror.spice b/tests/sim/testbenches/low_voltage_cmirror.spice index 75e4f606..e039b920 100644 --- a/tests/sim/testbenches/low_voltage_cmirror.spice +++ b/tests/sim/testbenches/low_voltage_cmirror.spice @@ -1,8 +1,8 @@ * Testbench: low-voltage cascode current mirror -* low_voltage_cmirror ports: IBIAS1 IBIAS2 GND IOUT1 IOUT2 -* IBIAS1 is the reference input node; IBIAS2 is an internally-generated -* second-tier bias (left to float to its own operating point); IOUT1/IOUT2 -* are the two mirrored output branches. +* subckt name: Low_voltage_current_mirror (filename stem is low_voltage_cmirror) +* ports: IBIAS1 IBIAS2 GND IOUT1 IOUT2 +* IBIAS1 and IBIAS2 are BOTH reference-current inputs (main + cascode bias); +* IOUT1/IOUT2 are the two mirrored output branches. * * Functional check: push a reference current into IBIAS1 and confirm both * output branches sink ~the same current (1:1 mirror) when held at mid-rail. @@ -13,17 +13,21 @@ * point is delicate -- calibrate the check bands against a golden ngspice run. .param VDD=1.8 .param IREF=10u -* Reference current into the IBIAS1 node. -Iref 0 IBIAS1 DC {IREF} +* This cell has TWO bias-current inputs: IBIAS1 (main mirror) and IBIAS2 +* (cascode level -- it is the Ib node of the second FVF and gates X4/X5). +* Both must be driven, or the output cascodes are starved and sink ~0. +* IBIAS2's value sets the cascode bias and may need a different magnitude +* than IREF -- calibrate against a golden run. +Iref 0 IBIAS1 DC {IREF} +Iref2 0 IBIAS2 DC {IREF} * Outputs held near mid-rail through ammeters; i(vmX) is the sunk mirror current. Vsup DD 0 DC 0.9 Vm1 DD IOUT1 DC 0 Vm2 DD IOUT2 DC 0 * DUT: IBIAS1 IBIAS2 GND IOUT1 IOUT2 * GND port tied straight to node 0 (no source -- a 0 V source here both ends -* on ground reads as a shorted VSRC and aborts the analysis). IBIAS2 left on -* its own net. -X1 IBIAS1 net_ibias2 0 IOUT1 IOUT2 low_voltage_cmirror +* on ground reads as a shorted VSRC and aborts the analysis). +X1 IBIAS1 IBIAS2 0 IOUT1 IOUT2 Low_voltage_current_mirror .dc Iref 2u 20u 1u .measure dc iout1_op FIND i(vm1) AT=10u .measure dc iout2_op FIND i(vm2) AT=10u