| #!/usr/bin/env python3 |
| #----------------------------------------------------------- |
| # |
| # Find all devices which have subcircuit definitions in the path |
| # skywater-pdk/libraries/sky130_fd_pr/VERSION/cells/. List all of |
| # these devices. Then find all paths from the directory models/ |
| # that will read the subcircuit definition through a hierarchical |
| # series of includes. |
| #----------------------------------------------------------- |
| |
| import re |
| import os |
| import sys |
| |
| #----------------------------------------------------------- |
| # Find all models in the model directory path, recursively |
| #----------------------------------------------------------- |
| |
| def addmodels(modelpath): |
| modelfmts = os.listdir(modelpath) |
| files_to_parse = [] |
| for modelfmt in modelfmts: |
| if os.path.isdir(modelpath + '/' + modelfmt): |
| files_to_parse.extend(addmodels(modelpath + '/' + modelfmt)) |
| else: |
| fmtext = os.path.splitext(modelfmt)[1] |
| if fmtext == '.spice': |
| files_to_parse.append(modelpath + '/' + modelfmt) |
| |
| return files_to_parse |
| |
| #----------------------------------------------------------- |
| # Find the device name in a SPICE "X" line |
| #----------------------------------------------------------- |
| |
| def get_device_name(line): |
| # The instance name has already been parsed out of the line, and |
| # all continuation lines have been added, so this routine finds |
| # the last keyword that is not a parameter (i.e., does not contain |
| # '='). |
| |
| tokens = line.split() |
| for token in tokens: |
| if '=' in token: |
| break |
| else: |
| devname = token |
| |
| return devname |
| |
| #----------------------------------------------------------- |
| # Pick the file from the list that is appropriate for the |
| # choice of either FEOL or BEOL corner. Mostly ad-hoc rules |
| # based on the known file names. |
| #----------------------------------------------------------- |
| |
| def choose_preferred(inclist, feol, beol, notop, debug): |
| |
| try: |
| # The top-level file 'sky130.lib.spice' is always preferred |
| incfile = next(item for item in inclist if os.path.split(item)[1] == 'sky130.lib.spice') |
| except: |
| # (1) Sort list by the length of the root name of the file |
| inclist = sorted(inclist, key=lambda x: len(os.path.splitext(os.path.split(x)[1])[0])) |
| |
| # (2) Sort list by depth of directory hierarchy |
| inclist = sorted(inclist, key=lambda x: len(x.split('/'))) |
| |
| for incfile in inclist: |
| incname = os.path.split(incfile)[1] |
| if debug: |
| print(' choose preferred, checking: "' + incname + '"') |
| |
| elif incname == 'custom.spice': |
| # Ignore "custom.spice" (example file, unused) |
| continue |
| elif notop and incname.startswith('correl'): |
| # Ignore "correl1.spice", etc., if "-notop" option chosen |
| continue |
| |
| elif feol in incname: |
| break |
| elif 't' in beol and 'typical' in incname: |
| break |
| elif 'h' in beol and 'high' in incname: |
| break; |
| elif 'l' in beol and 'low' in incname: |
| break; |
| |
| if debug: |
| incname = os.path.split(incfile)[1] |
| print(' choose_preferred: chose ' + incfile + ' (' + incname + ')') |
| return incfile |
| |
| #--------------------------------------------------------------------- |
| # Sort files with the subcircuit by relevance. Files are considered |
| # in the order ".model.spice", ".pm3.spice", and ".spice". |
| #--------------------------------------------------------------------- |
| |
| def preferred_order(subfiles, feol): |
| ordfiles = [] |
| feolstr = '__' + feol |
| |
| # Sort by length first, so shorter ones, e.g., without "leak" or |
| # "discrete", end up at the front of the list. |
| ordlist = sorted(subfiles, key=len) |
| |
| for file in ordlist[:]: |
| if file.endswith('.corner.spice') and feolstr in file: |
| ordfiles.append(file) |
| ordlist.remove(file) |
| |
| for file in ordlist[:]: |
| if file.endswith('.model.spice'): |
| ordfiles.append(file) |
| ordlist.remove(file) |
| |
| for file in ordlist[:]: |
| if file.endswith('.pm3.spice') and feolstr in file: |
| ordfiles.append(file) |
| ordlist.remove(file) |
| |
| for file in ordlist[:]: |
| if file.endswith('.pm3.spice'): |
| ordfiles.append(file) |
| ordlist.remove(file) |
| |
| ordfiles.extend(ordlist) |
| return ordfiles |
| |
| #----------------------------------------------------------- |
| # Find the appropriate file to include to handle this device |
| #----------------------------------------------------------- |
| |
| def check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug): |
| # subfiles = list of files that define this subcircuit. |
| # includedict = files that include the dictionary keyword file |
| # modfilesdict = files that are included by the dictionary keyword file |
| # feol = FEOL corner (fs, tt, ss, etc.) for transistors, diodes |
| # beol = BEOL corner (hl, tt, ll, etc.) for capacitors, resistors, inductors |
| |
| ordfiles = preferred_order(subfiles, feol) |
| if debug: |
| print('') |
| print('Check_device: Search order:') |
| for ordfile in ordfiles: |
| print(' ' + os.path.split(ordfile)[1]) |
| |
| # Find the proper include file to include this device. Attempt on all |
| # entries in ordfiles, and stop at the first one that returns a result. |
| # Assume that all files have unique names and point to the proper |
| # location, so that is is only necessary to look at the last path |
| # component. |
| |
| if debug: |
| print('\nClimb hierarchy of includes to the top:') |
| |
| for ordfile in ordfiles: |
| ordname = os.path.split(ordfile)[1] |
| if debug: |
| print(' (1) Search for "' + ordname + '"') |
| try: |
| inclist = includedict[ordname][1:] |
| except: |
| if debug: |
| print(' No include file found for "' + ordfile + '"') |
| print(' Sample entry:') |
| for key in includedict: |
| print(' ' + key + ': "' + str(includedict[key][1:]) + '"') |
| break |
| continue |
| else: |
| if debug: |
| print(' Starting list = ') |
| for item in inclist: |
| print(' ' + item) |
| |
| while True: |
| incfile = choose_preferred(inclist, feol, beol, notop, debug) |
| incname = os.path.split(incfile)[1] |
| if debug: |
| print(' (2) Search for "' + incname + '"') |
| try: |
| inclist = includedict[incname][1:] |
| except: |
| break |
| else: |
| if debug: |
| print(' Continuing list = ') |
| for item in inclist: |
| print(' ' + item) |
| |
| if debug: |
| print('Final top level include file is: "' + incfile + '"') |
| return incfile |
| |
| # Should only happen if subfiles is empty list |
| return None |
| |
| #----------------------------------------------------------- |
| # Find all cells and all models |
| #----------------------------------------------------------- |
| |
| def find_everything(pathtop): |
| cellspath = pathtop + '/cells' |
| modelspath = pathtop + '/models' |
| |
| allcells = os.listdir(cellspath) |
| |
| subcktrex = re.compile('\.subckt[ \t]+([^ \t]+)[ \t]+', re.IGNORECASE) |
| includerex = re.compile('\.include[ \t]+([^ \t]+)', re.IGNORECASE) |
| |
| filesdict = {} |
| subcktdict = {} |
| includedict = {} |
| modfilesdict = {} |
| |
| for cellfile in allcells: |
| cellpath = cellspath + '/' + cellfile |
| cellfmts = os.listdir(cellpath) |
| files_to_parse = [] |
| for cellfmt in cellfmts: |
| fmtext = os.path.splitext(cellfmt)[1] |
| if fmtext == '.spice': |
| files_to_parse.append(cellpath + '/' + cellfmt) |
| |
| for file in files_to_parse: |
| with open(file, 'r') as ifile: |
| spicelines = ifile.read().splitlines() |
| for line in spicelines: |
| smatch = subcktrex.match(line) |
| if smatch: |
| subname = smatch.group(1) |
| try: |
| subcktdict[subname].append(file) |
| except: |
| subcktdict[subname] = [file] |
| filetail = os.path.split(file)[1] |
| try: |
| filesdict[filetail].append(subname) |
| except: |
| filesdict[filetail] = [subname] |
| |
| files_to_parse = addmodels(modelspath) |
| files_to_parse.extend(addmodels(cellspath)) |
| |
| for file in files_to_parse: |
| # NOTE: Avoid problems with sonos directories using |
| # "tt.spice", which causes the include chain recursive |
| # loop to fail to exit. This is a one-off exception |
| # (hack alert) |
| if '_of_life' in file: |
| continue |
| |
| with open(file, 'r') as ifile: |
| spicelines = ifile.read().splitlines() |
| for line in spicelines: |
| imatch = includerex.match(line) |
| if imatch: |
| incname = imatch.group(1).strip('"') |
| inckey = os.path.split(incname)[1] |
| |
| try: |
| inclist = includedict[inckey] |
| except: |
| includedict[inckey] = [incname, file] |
| else: |
| if file not in inclist[1:]: |
| includedict[inckey].append(file) |
| filetail = os.path.split(file)[1] |
| try: |
| modlist = modfilesdict[filetail] |
| except: |
| modfilesdict[filetail] = [incname] |
| else: |
| if incname not in modlist: |
| modfilesdict[filetail].append(incname) |
| |
| return filesdict, subcktdict, includedict, modfilesdict |
| |
| #----------------------------------------------------------- |
| # Main application |
| #----------------------------------------------------------- |
| |
| def do_find_all_devices(pathtop, sourcefile, cellname=None, feol='tt', beol='tt', doall=False, notop=False, debug=False): |
| |
| (filesdict, subcktdict, includedict, modfilesdict) = find_everything(pathtop) |
| |
| if sourcefile: |
| # Parse the source file and find all 'X' records, and collect a list |
| # of all primitive devices used in the file by cross-checking against |
| # the dictionary of subcircuits. |
| |
| devrex = re.compile('x([^ \t]+)[ \t]+(.*)', re.IGNORECASE) |
| incfiles = [] |
| |
| with open(sourcefile, 'r') as ifile: |
| spicelines = ifile.read().splitlines() |
| |
| if debug: |
| print('Netlist file first line is "' + spicelines[0] + '"') |
| |
| isdev = False |
| for line in spicelines: |
| if line.startswith('*'): |
| continue |
| if line.strip() == '': |
| continue |
| elif line.startswith('+'): |
| if isdev: |
| rest += line[1:] |
| elif isdev: |
| devname = get_device_name(rest) |
| try: |
| subfiles = subcktdict[devname] |
| except: |
| pass |
| else: |
| incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) |
| if not incfile: |
| incfile = preferred_order(subfiles, feol)[0] |
| |
| if incfile: |
| if debug: |
| print('Device ' + devname + ': Include ' + incfile) |
| if incfile not in incfiles: |
| incfiles.append(incfile) |
| else: |
| print('Something went dreadfully wrong with device "' + devname + '"') |
| |
| isdev = False |
| |
| smatch = devrex.match(line) |
| if smatch: |
| instname = smatch.group(1) |
| rest = smatch.group(2) |
| isdev = True |
| elif isdev: |
| devname = get_device_name(rest) |
| try: |
| subfiles = subcktdict[devname] |
| except: |
| pass |
| else: |
| incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) |
| if not incfile: |
| incfile = preferred_order(subfiles, feol)[0] |
| |
| if incfile: |
| if debug: |
| print('Device "' + devname + '": Include "' + incfile + '"') |
| if incfile not in incfiles: |
| incfiles.append(incfile) |
| else: |
| print('Something went dreadfully wrong with device "' + devname + '"') |
| isdev = False |
| |
| # Return the .include lines needed |
| return incfiles |
| |
| elif cellname: |
| # Diagnostic: Given a cell name on the command line (with -cell=<name>), |
| # Run check_device() on the cell and report. |
| try: |
| subfiles = subcktdict[cellname] |
| except: |
| print('No cell "' + cellname + '" was found in the PDK files.') |
| sys.exit(1) |
| |
| incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) |
| if debug: |
| print('') |
| print('Report:') |
| print('') |
| print('Cell = "' + cellname + '"') |
| print('') |
| |
| bestfilepath = preferred_order(subfiles, feol)[0] |
| if bestfilepath.startswith(pathtop): |
| bestfile = bestfilepath[len(pathtop) + 1:] |
| print('Subcircuit defined in (from ' + pathtop + '/): "' + bestfile + '"') |
| |
| if debug: |
| print('') |
| print('Top level include: ') |
| |
| if incfile: |
| return [incfile] |
| else: |
| return [bestfilepath] |
| |
| elif doall: |
| allincludes = [] |
| for cellname in subcktdict: |
| |
| # Diagnostic: Given a cell name on the command line (with -cell=<name>), |
| # Run check_device() on the cell and report. |
| try: |
| subfiles = subcktdict[cellname] |
| except: |
| print('No cell "' + cellname + '" was found in the PDK files.') |
| continue |
| |
| incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) |
| print('Cell = "' + cellname + '"') |
| bestfilepath = preferred_order(subfiles, feol)[0] |
| if bestfilepath.startswith(pathtop): |
| bestfile = bestfilepath[len(pathtop) + 1:] |
| print(' Subcircuit: "' + os.path.split(bestfile)[1] + '"') |
| print(' Include: ', end='') |
| if incfile: |
| if incfile not in allincludes: |
| allincludes.append(incfile) |
| print('"' + incfile + '"') |
| else: |
| if bestfilepath not in allincludes: |
| allincludes.append(bestfilepath) |
| print('"' + bestfilepath + '"') |
| |
| print('') |
| print('Summary: All files to include:\n') |
| return allincludes |
| |
| else: |
| # No source file given, so just dump the lists of subcircuits, models, |
| # and files into four different output files. |
| |
| nsubs = 0 |
| with open('sublist.txt', 'w') as ofile: |
| for key in subcktdict: |
| nsubs += 1 |
| value = subcktdict[key] |
| print(key + ': ' + ', '.join(value), file=ofile) |
| |
| nfiles = 0 |
| with open('filelist.txt', 'w') as ofile: |
| for key in filesdict: |
| nfiles += 1 |
| value = filesdict[key] |
| print(key + ': ' + ', '.join(value), file=ofile) |
| |
| with open('inclist.txt', 'w') as ofile: |
| for key in includedict: |
| value = includedict[key] |
| print(key + '(' + value[0] + '): ' + ', '.join(value[1:]), file=ofile) |
| |
| with open('modfilelist.txt', 'w') as ofile: |
| for key in modfilesdict: |
| value = modfilesdict[key] |
| print(key + ': ' + ', '.join(value), file=ofile) |
| |
| print('Found ' + str(nsubs) + ' subcircuit definitions in ' + str(nfiles) + ' files.') |
| return [] |
| |
| #----------------------------------------------------------- |
| # Command-line entry point |
| #----------------------------------------------------------- |
| |
| if __name__ == "__main__": |
| |
| optionlist = [] |
| arguments = [] |
| |
| for option in sys.argv[1:]: |
| if option.find('-', 0) == 0: |
| optionlist.append(option) |
| else: |
| arguments.append(option) |
| |
| # Defaults: Set up for the most recent PDK version. |
| version = 'v0.20.1' |
| pathtop = '../../libraries/sky130_fd_pr/' + version |
| |
| # Default FEOL corner is "tt", and default BEOL corner is "tt" |
| feol = 'tt' |
| beol = 'tt' |
| cellname = None |
| debug = False |
| doall = False |
| notop = False |
| |
| # Override defaults from options |
| |
| for option in optionlist: |
| if option.startswith('-version'): |
| try: |
| version = option.split('=')[1] |
| except: |
| print('Option usage: -version=<versionstring>') |
| sys.exit(1) |
| elif option.startswith('-corner') or option.startswith('-feol'): |
| try: |
| feol = option.split('=')[1] |
| except: |
| print('Option usage: -feol=<corner_name>') |
| sys.exit(1) |
| elif option.startswith('-beol'): |
| try: |
| beol = option.split('=')[1] |
| except: |
| print('Option usage: -beol=<corner_name>') |
| sys.exit(1) |
| elif option.startswith('-cell'): |
| try: |
| cellname = option.split('=')[1] |
| except: |
| print('Option usage: -cell=<cell_name>') |
| sys.exit(1) |
| elif option == '-notop': |
| notop = True |
| elif option == '-all': |
| doall = True |
| elif option == '-debug': |
| debug = True |
| |
| # Parse "-pdkpath" after the others because it is dependent on any option |
| # "-version" passed on the command line. |
| |
| for option in optionlist: |
| if option.startswith('-pdkpath'): |
| try: |
| pathroot = option.split('=')[1] |
| except: |
| print('Option usage: -pdkpath=<pathname>') |
| sys.exit(1) |
| if not os.path.isdir(pathroot): |
| print('Cannot find PDK directory ' + pathroot) |
| sys.exit(1) |
| pathtop = pathroot + '/libraries/sky130_fd_pr/' + version |
| if not os.path.isdir(pathtop): |
| print('Cannot find primitive device directory ' + pathtop) |
| sys.exit(1) |
| |
| # To be done: Make this a useful routine that can insert one or more |
| # .include statements into a SPICE netlist. Should take any number of |
| # files on the arguments line and modify the files in place. |
| |
| if len(arguments) > 0: |
| sourcefile = arguments[0] |
| if not os.path.isfile(sourcefile): |
| print('Cannot read SPICE source file ' + sourcefile) |
| sys.exit(1) |
| else: |
| sourcefile = None |
| |
| if not os.path.isdir(pathtop): |
| print('Cannot find PDK path top level directory ' + pathtop) |
| sys.exit(1) |
| elif debug: |
| print('\nFinding everything in ' + pathtop + '.') |
| |
| incfiles = do_find_all_devices(pathtop, sourcefile, cellname, feol, beol, doall, notop, debug) |
| for incfile in incfiles: |
| if incfile.endswith('.lib.spice'): |
| print('.lib ' + incfile + ' ' + feol) |
| else: |
| print('.include "' + incfile + '"') |
| |
| sys.exit(0) |