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)