Added the netlist_to_layout python script that reads a SPICE netlist
and creates a device generator Tcl script that can be run through
magic to create all the devices, subcircuits, and ports as a starting
point for a layout. This has just been dropped in without any attempt
to integrate; it needs work to support both directory structure styles
from open_pdks (EF_STYLE 0 or 1), and for the schematic checks and
automatic netlist generation, needs to be switched from electric to
xschem---which will have to wait on xschem integration in open_pdks
(coming soon for sky130). The schematic tool independent part of this
(SPICE to magic) needs to be split out as an option that can be called
independently.
diff --git a/VERSION b/VERSION
index 814eec7..87903b6 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.0.92
+1.0.93
diff --git a/common/netlist_to_layout.py b/common/netlist_to_layout.py
new file mode 100755
index 0000000..a3d9f51
--- /dev/null
+++ b/common/netlist_to_layout.py
@@ -0,0 +1,364 @@
+#!/bin/env python3
+#-----------------------------------------------------------------------
+# netlist_to_layout.py
+#-----------------------------------------------------------------------
+#
+# Generate a magic layout from a SPICE netlist, running magic in batch
+# mode and calling up the PDK selections non-interactively for each
+# component in the netlist.
+#
+#---------------------------------------------------------------------
+# Written by Tim Edwards
+# efabless, inc.
+# November 17, 2016
+# Updated December 17, 2016
+# Version 1.0
+# Imported December 22, 2020 to open_pdks
+# To do: Rework from electric to xschem and support both EF_STYLE = 0
+# and 1 styles of directory structures from open_pdks.
+#---------------------------------------------------------------------
+
+import os
+import re
+import sys
+import subprocess
+
+# Routines to generate netlist from schematic if needed
+
+def check_schematic_out_of_date(spipath, schempath, schematic_name):
+ # Check if a netlist (spipath) is out-of-date relative to the schematics
+ # (schempath). Need to read the netlist and check all of the subcells.
+ need_capture = False
+ if not os.path.isfile(spipath):
+ return True
+ if os.path.isfile(schempath):
+ spi_statbuf = os.stat(spipath)
+ sch_statbuf = os.stat(schempath)
+ if spi_statbuf.st_mtime < sch_statbuf.st_mtime:
+ # netlist exists but is out-of-date
+ need_capture = True
+ else:
+ # only found that the top-level-schematic is older than the
+ # netlist. Now need to read the netlist, find all subcircuits,
+ # and check those dates, too.
+ schemdir = os.path.split(schempath)[0]
+ subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
+ with open(spipath, 'r') as ifile:
+ duttext = ifile.read()
+
+ dutlines = duttext.replace('\n+', ' ').splitlines()
+ for line in dutlines:
+ lmatch = subrex.match(line)
+ if lmatch:
+ subname = lmatch.group(1)
+ # NOTE: Electric uses library:cell internally to track libraries,
+ # and maps the ":" to "__" in the netlist. Not entirely certain that
+ # the double-underscore uniquely identifies the library:cell. . .
+ librex = re.compile('(.*)__(.*)', re.IGNORECASE)
+ lmatch = librex.match(subname)
+ if lmatch:
+ elecpath = os.path.split(os.path.split(schempath)[0])[0]
+ libname = lmatch.group(1)
+ subschem = elecpath + '/' + libname + '.delib/' + lmatch.group(2) + '.sch'
+ else:
+ libname = {}
+ subschem = schemdir + '/' + subname + '.sch'
+ # subcircuits that cannot be found in the current directory are
+ # assumed to be library components and therefore never out-of-date.
+ if os.path.exists(subschem):
+ sub_statbuf = os.stat(subschem)
+ if spi_statbuf.st_mtime < sub_statbuf.st_mtime:
+ # netlist exists but is out-of-date
+ need_capture = True
+ break
+ # mapping of characters to what's allowed in SPICE makes finding
+ # the associated schematic file a bit difficult. Requires wild-card
+ # searching.
+ elif libname:
+ restr = lmatch.group(2) + '.sch'
+ restr = restr.replace('.', '\.')
+ restr = restr.replace('_', '.')
+ schrex = re.compile(restr, re.IGNORECASE)
+ libpath = elecpath + '/' + libname + '.delib'
+ if os.path.exists(libpath):
+ liblist = os.listdir(libpath)
+ for file in liblist:
+ lmatch = schrex.match(file)
+ if lmatch:
+ subschem = libpath + '/' + file
+ sub_statbuf = os.stat(subschem)
+ if spi_statbuf.st_mtime < sch_statbuf.st_mtime:
+ # netlist exists but is out-of-date
+ need_capture = True
+ break
+ return need_capture
+
+def generate_schematic_netlist(schem_path, schem_src, project_path, schematic_name):
+ # Does schematic netlist exist and is it current?
+ if check_schematic_out_of_date(schem_path, schem_src, schematic_name):
+ # elec2spi will not run unless /spi/stub directory is present
+ if not os.path.exists(project_path + '/spi'):
+ os.makedirs(project_path + '/spi')
+ if not os.path.exists(project_path + '/spi/stub'):
+ os.makedirs(project_path + '/spi/stub')
+
+ # elec2spi will run, but not correctly, if the .java directory is not present
+ if not os.path.exists(project_path + '/elec/.java'):
+ # Same behavior as project manager. . . copy from skeleton directory
+ pdkdir = os.path.join(project_path, '.ef-config/techdir')
+ dotjava = os.path.join(pdkdir, 'libs.tech/deskel/dotjava')
+ if not os.path.exists(dotjava):
+ dotjava = '/ef/efabless/deskel/dotjava'
+
+ if os.path.exists(dotjava):
+ try:
+ shutil.copytree(dotjava, project_path + '/elec/.java', symlinks = True)
+ except IOError as e:
+ print('Error copying files: ' + str(e))
+
+ print('Generating schematic netlist.')
+ eproc = subprocess.Popen(['/ef/efabless/bin/elec2spi',
+ '-o', schem_path, '-LP', '-TS', '-NTI', schematic_name + '.delib',
+ schematic_name + '.sch'], stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT, cwd = project_path + '/elec/')
+ elecout = eproc.communicate()[0]
+ outlines = elecout.splitlines()
+ for line in outlines:
+ print(line)
+ if eproc.returncode != 0:
+ print('Bad result from elec2spi -o ' + schem_path + ' -LP -TS -NTI ' + schematic_name + '.delib ' + schematic_name + '.sch')
+ print('Failure to generate new schematic netlist.')
+ return False
+ return True
+
+def generate_layout_start(library):
+ # Write out a TCL script to generate the layout
+ ofile = open('create_script.tcl', 'w')
+
+ # Write a couple of simplifying procedures
+ print('#!/usr/bin/env wish', file=ofile)
+ print('#-------------------------------------', file=ofile)
+ print('# Script to create layout from netlist', file=ofile)
+ print('# Source this in magic.', file=ofile)
+ print('#-----------------------------------------', file=ofile)
+ print('proc move_forward {instname} {', file=ofile)
+ print(' select cell $instname', file=ofile)
+ print(' set anum [lindex [array -list count] 1]', file=ofile)
+ print(' set xpitch [lindex [array -list pitch] 0]', file=ofile)
+ print(' set bbox [box values]', file=ofile)
+ print(' set posx [lindex $bbox 0]', file=ofile)
+ print(' set posy [lindex $bbox 1]', file=ofile)
+ print(' set width [expr [lindex $bbox 2] - $posx]', file=ofile)
+ print(' set posx [expr $posx + $width + $xpitch * $anum]', file=ofile)
+ print(' box position ${posx}i ${posy}i', file=ofile)
+ print(' return [lindex $bbox 3]', file=ofile)
+ print('}', file=ofile)
+ print('', file=ofile)
+ print('proc get_and_move_inst {cellname instname anum} {', file=ofile)
+ print(' set newinst [getcell $cellname]', file=ofile)
+ print(' select cell $newinst', file=ofile)
+ print(' if {$newinst == ""} {return}', file=ofile)
+ print(' identify $instname', file=ofile)
+ print(' if {$anum > 1} {array 1 $anum}', file=ofile)
+ print(' set bbox [box values]', file=ofile)
+ print(' set posx [lindex $bbox 2]', file=ofile)
+ print(' set posy [lindex $bbox 1]', file=ofile)
+ print(' box position ${posx}i ${posy}i', file=ofile)
+ print(' return [lindex $bbox 3]', file=ofile)
+ print('}', file=ofile)
+ print('', file=ofile)
+ print('proc add_pin {pinname portnum} {', file=ofile)
+ print(' box size 1um 1um', file=ofile)
+ print(' paint m1', file=ofile)
+ print(' label $pinname FreeSans 16 0 0 0 c m1', file=ofile)
+ print(' port make $portnum', file=ofile)
+ print(' box move s 2um', file=ofile)
+ print('}', file=ofile)
+ print('', file=ofile)
+ if not library:
+ print('namespace import ${PDKNAMESPACE}::*', file=ofile)
+ print('suspendall', file=ofile)
+ return ofile
+
+def generate_layout_add(ofile, subname, subpins, complist, library):
+ parmrex = re.compile('([^=]+)=([^=]+)', re.IGNORECASE)
+ exprrex = re.compile('\'([^\']+)\'', re.IGNORECASE)
+ librex = re.compile('(.*)__(.*)', re.IGNORECASE)
+
+ print('load ' + subname, file=ofile)
+ print('box 0um 0um 0um 0um', file=ofile)
+ print('', file=ofile)
+
+ # Generate all of the pins as labels
+ pinlist = subpins.split()
+ i = 0
+ for pin in pinlist:
+ # Escape [ and ] in pin name
+ pin_esc = pin.replace('[', '\[').replace(']', '\]')
+ # To be done: watch for key=value parameters
+ print('add_pin ' + pin_esc + ' ' + str(i), file=ofile)
+ i += 1
+
+ # Set initial position for importing cells
+ print('box size 0 0', file=ofile)
+ print('set posx 0', file=ofile)
+ print('set posy [expr {round(3 / [cif scale out])}]', file=ofile)
+ print('box position ${posx}i ${posy}i', file=ofile)
+
+ for comp in complist:
+ params = {}
+ tokens = comp.split()
+ # Diagnostic
+ # print("Adding component " + tokens[0])
+ instname = tokens[0]
+ mult = 1
+ for token in tokens[1:]:
+ rmatch = parmrex.match(token)
+ if rmatch:
+ parmname = rmatch.group(1).upper()
+ parmval = rmatch.group(2)
+ params[parmname] = parmval
+ if parmname.upper() == 'M':
+ try:
+ mult = int(parmval)
+ except ValueError:
+ # This takes care of multiplier expressions, as long
+ # as they don't reference parameter names themselves.
+ mult = eval(eval(parmval))
+ else:
+ # Last one that isn't a parameter will be kept
+ devtype = token
+
+ # If devtype is a cellname in the form "<lib>__<cell>" then check if <cell> is
+ # in the user's /ip/ directory. If so, recast devtype to just <cell>.
+ ematch = librex.match(devtype)
+ if ematch:
+ cellname = ematch.group(2)
+ if os.path.exists(os.path.expanduser('~/design/ip/' + cellname)):
+ devtype = cellname
+
+ # devtype is assumed to be in library. If not, it will throw an error and
+ # attempt to use 'getcell' on devtype. NOTE: Current usage is to not pass
+ # a library to netlist_to_layout.py but to rely on the PDK Tcl script to
+ # define variable PDKNAMESPACE, which is the namespace to use for low-level
+ # components, and may not be the same name as the technology node.
+ if library:
+ libdev = library + '::' + devtype
+ else:
+ libdev = '${PDKNAMESPACE}::' + devtype
+ outparts = []
+ outparts.append('magic::gencell ' + libdev + ' ' + instname)
+
+ # Output all parameters. Parameters not used by the toolkit are ignored
+ # by the toolkit.
+ outparts.append('-spice')
+ for item in params:
+ outparts.append(str(item).lower())
+ outparts.append(params[item])
+
+ outstring = ' '.join(outparts)
+ print('if {[catch {' + outstring + '}]} {', file=ofile)
+ print(' get_and_move_inst ' + devtype + ' ' + instname
+ + ' ' + str(mult), file=ofile)
+ print('} else {', file=ofile)
+ print(' move_forward ' + instname, file=ofile)
+ print('}', file=ofile)
+ print('', file=ofile)
+ print('save ' + subname, file=ofile)
+
+def generate_layout_end(ofile):
+ print('resumeall', file=ofile)
+ print('refresh', file=ofile)
+ print('writeall force', file=ofile)
+ print('quit', file=ofile)
+ ofile.close()
+
+if __name__ == '__main__':
+
+ # Parse command line for options and arguments
+ options = []
+ arguments = []
+ for item in sys.argv[1:]:
+ if item.find('-', 0) == 0:
+ options.append(item)
+ else:
+ arguments.append(item)
+
+ if len(arguments) > 0:
+ inputfile = arguments[0]
+ if len(arguments) > 1:
+ library = arguments[1]
+ else:
+ library = None
+ else:
+ raise SyntaxError('Usage: ' + sys.argv[0] + ' netlist_file [library] [-options]\n')
+
+ debug = False
+ for item in options:
+ result = item.split('=')
+ if result[0] == '-help':
+ print('Usage: ' + sys.argv[0] + ' netlist_file [-options]\n')
+ elif result[0] == '-debug':
+ debug = True
+ else:
+ raise SyntaxError('Bad option ' + item + ', options are -help\n')
+
+ # Check if netlist exists or needs updating.
+ netpath = os.path.split(inputfile)[0]
+ netfile = os.path.split(inputfile)[1]
+ netname = os.path.splitext(netfile)[0]
+ projectpath = os.path.split(netpath)[0]
+ projectname = os.path.split(projectpath)[1]
+
+ elec_path = projectpath + '/elec/' + projectname + '.delib'
+ schem_src = elec_path + '/' + projectname + '.sch'
+
+ if not generate_schematic_netlist(inputfile, schem_src, projectpath, netname):
+ raise SyntaxError('File ' + inputfile + ': Failure to generate netlist.')
+
+ # Read SPICE netlist
+
+ with open(inputfile, 'r') as ifile:
+ spicetext = ifile.read()
+
+ subrex = re.compile('.subckt[ \t]+(.*)$', re.IGNORECASE)
+ # All devices are going to be subcircuits
+ xrex = re.compile('[xmcrbdi]([^ \t]+)[ \t](.*)$', re.IGNORECASE)
+ namerex = re.compile('([^= \t]+)[ \t]+(.*)$', re.IGNORECASE)
+ endsrex = re.compile('^[ \t]*\.ends', re.IGNORECASE)
+
+ # Contatenate continuation lines
+ spicelines = spicetext.replace('\n+', ' ').splitlines()
+
+ insub = False
+ subname = ''
+ subpins = ''
+ complist = []
+ ofile = generate_layout_start(library)
+ for line in spicelines:
+ if not insub:
+ lmatch = subrex.match(line)
+ if lmatch:
+ rest = lmatch.group(1)
+ smatch = namerex.match(rest)
+ if smatch:
+ subname = smatch.group(1)
+ subpins = smatch.group(2)
+ insub = True
+ else:
+ raise SyntaxError('File ' + inputfile + ': Failure to parse line ' + line)
+ break
+ else:
+ lmatch = endsrex.match(line)
+ if lmatch:
+ insub = False
+ generate_layout_add(ofile, subname, subpins, complist, library)
+ subname = None
+ subpins = None
+ complist = []
+ else:
+ xmatch = xrex.match(line)
+ if xmatch:
+ complist.append(line)
+
+ generate_layout_end(ofile)