Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
214 changes: 214 additions & 0 deletions src/orfs_integration/netlist_to_verilog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
"""
netlist_to_verilog.py

Converts a validated eSim .cir.out netlist into synthesizable Verilog.
Works on top of pdk_validator to reuse the parsed netlist structure.
"""

import sys
from pathlib import Path
from pdk_validator import SpiceNetlist, ANALOG_PRIMITIVES


# Maps sky130 cell names to their Verilog primitive equivalents
# Format: cell_name -> (verilog_primitive, output_port, input_ports)
CELL_TO_VERILOG = {
# Inverters
"sky130_fd_sc_hd__inv_1": ("not", "Y", ["A"]),
"sky130_fd_sc_hd__inv_2": ("not", "Y", ["A"]),
"sky130_fd_sc_hd__inv_4": ("not", "Y", ["A"]),
"sky130_fd_sc_hd__inv_8": ("not", "Y", ["A"]),
# Buffers
"sky130_fd_sc_hd__buf_1": ("buf", "X", ["A"]),
"sky130_fd_sc_hd__buf_2": ("buf", "X", ["A"]),
"sky130_fd_sc_hd__buf_4": ("buf", "X", ["A"]),
"sky130_fd_sc_hd__buf_8": ("buf", "X", ["A"]),
# NAND
"sky130_fd_sc_hd__nand2_1": ("nand", "Y", ["A", "B"]),
"sky130_fd_sc_hd__nand2_2": ("nand", "Y", ["A", "B"]),
"sky130_fd_sc_hd__nand2_4": ("nand", "Y", ["A", "B"]),
"sky130_fd_sc_hd__nand3_1": ("nand", "Y", ["A", "B", "C"]),
"sky130_fd_sc_hd__nand3_2": ("nand", "Y", ["A", "B", "C"]),
"sky130_fd_sc_hd__nand4_1": ("nand", "Y", ["A", "B", "C", "D"]),
# NOR
"sky130_fd_sc_hd__nor2_1": ("nor", "Y", ["A", "B"]),
"sky130_fd_sc_hd__nor2_2": ("nor", "Y", ["A", "B"]),
"sky130_fd_sc_hd__nor3_1": ("nor", "Y", ["A", "B", "C"]),
"sky130_fd_sc_hd__nor4_1": ("nor", "Y", ["A", "B", "C", "D"]),
# AND
"sky130_fd_sc_hd__and2_0": ("and", "X", ["A", "B"]),
"sky130_fd_sc_hd__and2_2": ("and", "X", ["A", "B"]),
"sky130_fd_sc_hd__and2_4": ("and", "X", ["A", "B"]),
"sky130_fd_sc_hd__and3_1": ("and", "X", ["A", "B", "C"]),
"sky130_fd_sc_hd__and4_1": ("and", "X", ["A", "B", "C", "D"]),
# OR
"sky130_fd_sc_hd__or2_0": ("or", "X", ["A", "B"]),
"sky130_fd_sc_hd__or2_2": ("or", "X", ["A", "B"]),
"sky130_fd_sc_hd__or2_4": ("or", "X", ["A", "B"]),
"sky130_fd_sc_hd__or3_1": ("or", "X", ["A", "B", "C"]),
"sky130_fd_sc_hd__or4_1": ("or", "X", ["A", "B", "C", "D"]),
# XOR
"sky130_fd_sc_hd__xor2_1": ("xor", "X", ["A", "B"]),
"sky130_fd_sc_hd__xor2_2": ("xor", "X", ["A", "B"]),
"sky130_fd_sc_hd__xnor2_1": ("xnor", "Y", ["A", "B"]),
"sky130_fd_sc_hd__xnor2_2": ("xnor", "Y", ["A", "B"]),
# Flip-flops (D type, positive edge)
"sky130_fd_sc_hd__dfxtp_1": ("dff", "Q", ["D", "CLK"]),
"sky130_fd_sc_hd__dfxtp_2": ("dff", "Q", ["D", "CLK"]),
"sky130_fd_sc_hd__dfxtp_4": ("dff", "Q", ["D", "CLK"]),
# Clock buffers (treated as regular buffers in Verilog)
"sky130_fd_sc_hd__clkbuf_1": ("buf", "X", ["A"]),
"sky130_fd_sc_hd__clkbuf_2": ("buf", "X", ["A"]),
"sky130_fd_sc_hd__clkbuf_4": ("buf", "X", ["A"]),
"sky130_fd_sc_hd__clkbuf_8": ("buf", "X", ["A"]),
}


def get_digital_ports(subckt_ports):
# Filter out power/ground pins — not needed in synthesizable Verilog
skip = {"vdd", "vcc", "gnd", "vss", "vpwr", "vgnd"}
return [p for p in subckt_ports if p.lower() not in skip]


def infer_port_direction(port, instances):
# If a port drives an instance input it's an input, if it comes from an output it's output
# Simple heuristic: assume first ports are inputs, last is output
# A proper implementation would do connectivity analysis
driven_nets = set()
for inst in instances:
parts = inst["raw"].split()
positional = [p for p in parts[1:] if "=" not in p]
if len(positional) >= 2:
# Last positional before power pins is the cell — nets are everything else
cell = positional[-1]
nets = positional[:-1]
# First net after instance name is usually output for most gates
if nets:
driven_nets.add(nets[0])
return driven_nets


def convert_subckt_to_verilog(subckt, internal_subckt_names):
name = subckt["name"]
ports = get_digital_ports(subckt["ports"])
instances = subckt["instances"]

# Collect all internal wire names
all_nets = set()
for inst in instances:
parts = inst["raw"].split()
positional = [p for p in parts[1:] if "=" not in p]
# exclude cell name and power pins
skip = {"vdd", "vcc", "gnd", "vss", "vpwr", "vgnd"}
nets = [p for p in positional[:-1] if p.lower() not in skip]
all_nets.update(nets)

internal_wires = [n for n in all_nets if n not in ports]

lines = []
lines.append(f"module {name} (")
lines.append(f" {', '.join(ports)}")
lines.append(f");")
lines.append("")

# Port declarations — simple heuristic: last port is output, rest are inputs
if ports:
for p in ports[:-1]:
lines.append(f" input wire {p};")
lines.append(f" output wire {ports[-1]};")

if internal_wires:
lines.append("")
for w in sorted(internal_wires):
lines.append(f" wire {w};")

lines.append("")

# Instance declarations
for inst in instances:
ctype = inst["type"]
cell = inst["cell"]
iname = inst["name"]

# Skip analog primitives
if ctype in ANALOG_PRIMITIVES:
continue

# Skip power sources
if ctype == "v":
continue

parts = inst["raw"].split()
positional = [p for p in parts[1:] if "=" not in p]
skip_pins = {"vdd", "vcc", "gnd", "vss", "vpwr", "vgnd"}
nets = [p for p in positional[:-1] if p.lower() not in skip_pins]

if cell in internal_subckt_names:
# Hierarchical instance — emit as module instantiation
lines.append(f" {cell} {iname} (")
port_connects = [f" .port{i}({n})" for i, n in enumerate(nets)]
lines.append(",\n".join(port_connects))
lines.append(f" );")
elif cell in CELL_TO_VERILOG:
primitive, out_port, in_ports = CELL_TO_VERILOG[cell]

if primitive == "dff":
# Emit always block for flip-flop
if len(nets) >= 2:
q_net, d_net, clk_net = nets[0], nets[1], nets[2] if len(nets) > 2 else "clk"
lines.append(f" // {iname} : D flip-flop")
lines.append(f" reg {q_net}_reg;")
lines.append(f" always @(posedge {clk_net}) {q_net}_reg <= {d_net};")
lines.append(f" assign {q_net} = {q_net}_reg;")
else:
# Gate primitive
if nets:
out_net = nets[0]
in_nets = nets[1:] if len(nets) > 1 else nets
lines.append(f" {primitive} {iname} ({out_net}, {', '.join(in_nets)});")
else:
lines.append(f" // WARNING: {iname} uses unmapped cell '{cell}' - skipped")

lines.append("")
lines.append("endmodule")
lines.append("")
return "\n".join(lines)


def convert(cir_out_path, output_path=None):
netlist = SpiceNetlist(cir_out_path)

if not netlist.subcircuits:
print("[netlist_to_verilog] No subcircuits found in netlist.")
return None

if output_path is None:
output_path = str(Path(cir_out_path).with_suffix(".v"))

verilog_blocks = []
verilog_blocks.append(f"// Auto-generated Verilog from eSim netlist")
verilog_blocks.append(f"// Source: {Path(cir_out_path).name}")
verilog_blocks.append(f"// Tool : eSim-ORFS netlist_to_verilog.py")
verilog_blocks.append("")

for subckt in netlist.subcircuits:
print(f"[netlist_to_verilog] Converting subcircuit: {subckt['name']}")
verilog_blocks.append(
convert_subckt_to_verilog(subckt, netlist.internal_subckt_names)
)

verilog_output = "\n".join(verilog_blocks)
Path(output_path).write_text(verilog_output)
print(f"[netlist_to_verilog] Verilog written to: {output_path}")
return output_path


if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python netlist_to_verilog.py <netlist.cir.out> [output.v]")
sys.exit(1)
out = convert(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)
if out:
print("\n--- Generated Verilog ---")
print(Path(out).read_text())

73 changes: 73 additions & 0 deletions src/orfs_integration/orfs_config_gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import sys
from pathlib import Path
from datetime import datetime
from pdk_validator import validate_netlist, PDKValidator

PDK_DEFAULTS = {
"sky130": {
"PLATFORM": "sky130hd",
"SCL": "sky130_fd_sc_hd",
"TARGET_DENSITY": "0.65",
"CLOCK_PERIOD": "10.0",
"CORE_UTILIZATION": "40",
"CORE_ASPECT_RATIO": "1",
"CORE_MARGIN": "2",
}
}

def generate_config(cir_out_path, design_name=None, pdk="sky130",
clock_port="clk", clock_period_ns=None, output_dir="."):
print(f"[orfs_config_gen] Validating {cir_out_path} against {pdk} PDK...")
report = validate_netlist(cir_out_path, pdk)
PDKValidator.print_report(report)
if report["summary"] == "FAIL":
raise RuntimeError(f"Validation FAILED - {len(report['unmappable'])} unmappable primitive(s).")
if design_name is None:
design_name = Path(cir_out_path).stem.replace(".cir", "").replace(".", "_")
defaults = PDK_DEFAULTS.get(pdk, PDK_DEFAULTS["sky130"])
clk_period = str(clock_period_ns) if clock_period_ns else defaults["CLOCK_PERIOD"]
lines = [
f"# ORFS config.mk - auto-generated by eSim-ORFS integration",
f"# Source : {Path(cir_out_path).resolve()}",
f"# Generated : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"# PDK : {pdk}", f"",
f"export DESIGN_NAME = {design_name}",
f"export PLATFORM = {defaults['PLATFORM']}", f"",
f"export VERILOG_FILES = $(DESIGN_HOME)/src/$(DESIGN_NAME)/$(DESIGN_NAME).v",
f"export SDC_FILE = $(DESIGN_HOME)/src/$(DESIGN_NAME)/constraint.sdc",
f"export ABC_AREA = 0", f"",
f"export CORE_UTILIZATION = {defaults['CORE_UTILIZATION']}",
f"export CORE_ASPECT_RATIO = {defaults['CORE_ASPECT_RATIO']}",
f"export CORE_MARGIN = {defaults['CORE_MARGIN']}", f"",
f"export CLOCK_PORT = {clock_port}",
f"export CLOCK_PERIOD = {clk_period}", f"",
f"export TARGET_DENSITY = {defaults['TARGET_DENSITY']}", f"",
f"# Mapped cells ({len(report['valid_cells'])}):",
]
for cell in report["valid_cells"]:
lines.append(f"# {cell['name']} -> {cell['cell']}")
if report["analog_primitives"]:
lines.append(f"# Skipped analog ({len(report['analog_primitives'])}):")
for a in report["analog_primitives"]:
lines.append(f"# {a['name']} ({a['type'].upper()})")
out_path = Path(output_dir) / "config.mk"
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text("\n".join(lines) + "\n")
print(f"[orfs_config_gen] config.mk written to: {out_path.resolve()}")
return str(out_path)

if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("netlist")
parser.add_argument("--design")
parser.add_argument("--pdk", default="sky130")
parser.add_argument("--clock-port", default="clk")
parser.add_argument("--clock-period", type=float)
parser.add_argument("--output-dir", default=".")
args = parser.parse_args()
try:
generate_config(args.netlist, args.design, args.pdk, args.clock_port, args.clock_period, args.output_dir)
except RuntimeError as e:
print(f"\n[ERROR] {e}")
sys.exit(1)
Loading