| #!/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) |