blob: 0c228ef74eabdb1cf292a9601736d4b3e7b3a080 [file] [log] [blame]
#!/usr/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)