| #!/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 |
| # Updated February 10, 2021 for use running on netlist alone |
| #--------------------------------------------------------------------- |
| |
| import os |
| import re |
| import sys |
| import subprocess |
| |
| def generate_layout_start(library, ofile=sys.stdout): |
| global debugmode |
| if debugmode: |
| print('Writing layout generating script.') |
| |
| # 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('drc off', 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(subname, subpins, complist, library, ofile=sys.stdout): |
| global debugmode |
| if debugmode: |
| if subpins: |
| print(' Generating layout for subcircuit ' + subname + '.') |
| else: |
| print(' Generating layout for top level circuit ' + subname + '.') |
| |
| gparmrex = re.compile('([^= \t]+)=([^=]+)') |
| sparmrex = re.compile('([^= \t]+)=([^= \t]+)[ \t]*(.*)') |
| expr1rex = re.compile('([^= \t]+)=\'([^\']+)\'[ \t]*(.*)') |
| expr2rex = re.compile('([^= \t]+)=\{([^\}]+)\}[ \t]*(.*)') |
| tokrex = re.compile('([^ \t]+)[ \t]*(.*)') |
| |
| if subname: |
| print('load ' + subname + ' -quiet', file=ofile) |
| |
| print('box 0um 0um 0um 0um', file=ofile) |
| print('', file=ofile) |
| |
| # Generate all of the pins as labels |
| if subpins: |
| 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: |
| pinlist = [] |
| paramlist = [] |
| |
| # Parse into pins, device name, and parameters. Make sure parameters |
| # incorporate quoted expressions as {} or ''. |
| rest = comp |
| while rest and rest != '': |
| gmatch = gparmrex.match(rest) |
| if gmatch: |
| break |
| else: |
| tmatch = tokrex.match(rest) |
| if tmatch: |
| token = tmatch.group(1) |
| pinlist.append(token) |
| rest = tmatch.group(2) |
| else: |
| rest = '' |
| |
| while rest and rest != '': |
| ematch = expr1rex.match(rest) |
| if ematch: |
| pname = ematch.group(1) |
| value = ematch.group(2) |
| paramlist.append((pname, '{' + value + '}')) |
| rest = ematch.group(3) |
| else: |
| ematch = expr2rex.match(rest) |
| if ematch: |
| pname = ematch.group(1) |
| value = ematch.group(2) |
| paramlist.append((pname, '{' + value + '}')) |
| rest = ematch.group(3) |
| else: |
| smatch = sparmrex.match(rest) |
| if smatch: |
| pname = smatch.group(1) |
| value = smatch.group(2) |
| paramlist.append((pname, value)) |
| rest = smatch.group(3) |
| else: |
| print('Error parsing line "' + comp + '"') |
| print('at: "' + rest + '"') |
| rest = '' |
| |
| if len(pinlist) < 2: |
| print('Error: No device type found in line "' + comp + '"') |
| print('Tokens found are: ' + ', '.join(pinlist)) |
| continue |
| |
| instname = pinlist[0] |
| devtype = pinlist[-1] |
| pinlist = pinlist[0:-1] |
| |
| # Diagnostic |
| if debugmode: |
| print(' Adding component ' + devtype + ' instance ' + instname) |
| |
| mult = 1 |
| for param in paramlist: |
| parmname = param[0] |
| parmval = param[1] |
| 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)) |
| |
| # devtype is assumed to be in library. If not, it will 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 param in paramlist: |
| outparts.append(str(param[0]).lower()) |
| outparts.append(param[1]) |
| |
| 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=sys.stdout): |
| global debugmode |
| |
| print('resumeall', file=ofile) |
| print('writeall force', file=ofile) |
| print('quit -noprompt', file=ofile) |
| |
| def parse_layout(topname, lines, library, ofile): |
| global debugmode |
| |
| subrex = re.compile('.subckt[ \t]+(.*)$', re.IGNORECASE) |
| devrex = re.compile('[xmcrbdivq]([^ \t]+)[ \t](.*)$', re.IGNORECASE) |
| namerex = re.compile('([^= \t]+)[ \t]+(.*)$', re.IGNORECASE) |
| endsrex = re.compile('^[ \t]*\.ends', re.IGNORECASE) |
| |
| insub = False |
| subname = '' |
| subpins = '' |
| complist = [] |
| toplist = [] |
| |
| for line in lines: |
| 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: |
| print('File ' + inputfile + ': Failure to parse line ' + line) |
| else: |
| dmatch = devrex.match(line) |
| if dmatch: |
| toplist.append(line) |
| else: |
| lmatch = endsrex.match(line) |
| if lmatch: |
| insub = False |
| generate_layout_add(subname, subpins, complist, library, ofile) |
| subname = None |
| subpins = None |
| complist = [] |
| else: |
| dmatch = devrex.match(line) |
| if dmatch: |
| complist.append(line) |
| |
| # Add any top-level components |
| if toplist: |
| generate_layout_add(topname, None, toplist, library, ofile) |
| |
| def usage(): |
| print('Usage:') |
| print(' netlist_to_layout.py <filename> [<namespace>] [-options]') |
| print('') |
| print('Arguments:') |
| print(' <filename> is the path to the SPICE netlist to import to magic') |
| print(' <namespace> is the namespace of the PDK') |
| print('') |
| print('Options:') |
| print(' -keep Keep the working script after completion.') |
| print(' -debug Provide verbose output while generating script.') |
| print(' -help Print this help text.') |
| |
| # Main procedure |
| |
| if __name__ == '__main__': |
| global debugmode |
| |
| # Parse command line for options and arguments |
| optionlist = [] |
| arguments = [] |
| for item in sys.argv[1:]: |
| if item.find('-', 0) == 0: |
| optionlist.append(item) |
| else: |
| arguments.append(item) |
| |
| if len(arguments) > 0: |
| inputfile = arguments[0] |
| if len(arguments) > 1: |
| library = arguments[1] |
| else: |
| library = None |
| else: |
| usage() |
| sys.exit(0) |
| |
| debugmode = False |
| keepmode = False |
| |
| for item in optionlist: |
| result = item.split('=') |
| if result[0] == '-help': |
| usage() |
| sys.exit(0) |
| elif result[0] == '-debug': |
| debugmode = True |
| elif result[0] == '-keep': |
| keepmode = True |
| else: |
| usage() |
| sys.exit(1) |
| |
| netpath = os.path.split(inputfile)[0] |
| if netpath == '': |
| netpath = os.getcwd() |
| |
| if os.path.splitext(inputfile)[1] == '.sch': |
| print('Sorry, automatic conversion of schematic to netlist not yet supported.') |
| sys.exit(1) |
| |
| netroot = os.path.split(netpath)[0] |
| magpath = os.path.join(netroot, 'mag') |
| if not os.path.isdir(magpath): |
| print('Error: Layout path "' + magpath + '" does not exist or is not readable.') |
| sys.exit(1) |
| |
| # NOTE: There should be some attempt to find the installed PDK magicrc file |
| # if there is no mag/ directory. |
| rcfile = '.magicrc' |
| rcfilepath = os.path.join(magpath, rcfile) |
| if not os.path.isfile(rcfilepath): |
| print('Error: No startup script file "' + rcfilepath + '"') |
| sys.exit(1) |
| |
| # Read SPICE netlist |
| with open(inputfile, 'r') as ifile: |
| if debugmode: |
| print('Reading file ' + inputfile) |
| spicetext = ifile.read() |
| |
| # Contatenate continuation lines |
| spicelines = spicetext.replace('\n+', ' ').splitlines() |
| |
| filename = os.path.split(inputfile)[1] |
| topname = os.path.splitext(filename)[0] |
| |
| scriptfile = 'generate_layout.tcl' |
| scriptpath = os.path.join(magpath, scriptfile) |
| |
| with open(scriptpath, 'w') as ofile: |
| generate_layout_start(library, ofile) |
| parse_layout(topname, spicelines, library, ofile) |
| generate_layout_end(ofile) |
| |
| myenv = os.environ.copy() |
| myenv['MAGTYPE'] = 'mag' |
| |
| # Run the layout generator |
| mproc = subprocess.run(['magic', '-dnull', '-noconsole', '-rcfile', |
| rcfile, scriptfile], |
| stdin = subprocess.DEVNULL, stdout = subprocess.PIPE, |
| stderr = subprocess.PIPE, cwd = magpath, |
| env = myenv, universal_newlines = True) |
| if mproc.stdout: |
| for line in mproc.stdout.splitlines(): |
| print(line) |
| if mproc.stderr: |
| print('Error message output from magic:') |
| for line in mproc.stderr.splitlines(): |
| print(line) |
| if mproc.returncode != 0: |
| print('ERROR: Magic exited with status ' + str(mproc.returncode)) |
| |
| # Clean up |
| if not keepmode: |
| os.remove(scriptpath) |
| |
| sys.exit(0) |