| #!/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 pprint |
| import sys |
| |
| __dir__ = os.path.dirname(os.path.abspath(__file__)) |
| sys.path.append(os.path.abspath(os.path.join(__dir__, ".."))) |
| |
| from common import get_cell_directory |
| |
| |
| SPICE_X_DEF = re.compile('^(x.*?)$', flags=re.MULTILINE|re.I) |
| |
| |
| def extract_x(data): |
| """ |
| |
| >>> extract_x(''' |
| ... xsky130_fd_pr__pfet_01v8 d g s b sky130_fd_pr__pfet_01v8__model l = {l} w = {w} ad = {ad} as = {as} pd = {pd} ps = {ps} nrd = {nrd} nrs = {nrs} sa = {sa} sb = {sb} sd = {sd} nf = {nf} |
| ... ''') |
| ['sky130_fd_pr__pfet_01v8__model'] |
| |
| >>> extract_x(''' |
| ... X0 Drain Gate Source BULK sky130_fd_pr__pfet_01v8 w=840000u l=150000u |
| ... ''') |
| ['sky130_fd_pr__pfet_01v8'] |
| """ |
| |
| defs = [] |
| for m in SPICE_X_DEF.finditer(data): |
| xdef = m.group(0).split() |
| |
| i = 0 |
| while i < len(xdef)-1: |
| if '=' in xdef[i+1]: |
| break |
| if i+2 < len(xdef) and xdef[i+2] == '=': |
| break |
| i += 1 |
| defs.append(xdef[i]) |
| return defs |
| |
| incs = [] |
| for m in SPICE_INC.finditer(data): |
| print('INC', m.group(1)) |
| |
| |
| SPICE_MODEL = re.compile('^.model +([^.]+)(.[0-9]*)? (.*?)$', flags=re.MULTILINE) |
| SPICE_SUBCKT = re.compile('^.subckt +([^ ]+) (.*?)$', flags=re.MULTILINE) |
| |
| |
| def extract_defines(data): |
| """ |
| |
| >>> extract_defines(''' |
| ... .subckt sky130_fd_pr__rf_nfet_g5v0d10v5_bM02 d g s b |
| ... ''') |
| ['sky130_fd_pr__rf_nfet_g5v0d10v5_bM02'] |
| |
| >>> extract_defines(''' |
| ... .model sky130_fd_pr__nfet_05v0_nvt__model.9 nmos |
| ... .model sky130_fd_pr__nfet_05v0_nvt__model.10 nmos |
| ... ''') |
| ['sky130_fd_pr__nfet_05v0_nvt__model'] |
| |
| """ |
| defines = set() |
| for m in SPICE_MODEL.finditer(data): |
| defines.add(m.group(1)) |
| for m in SPICE_SUBCKT.finditer(data): |
| defines.add(m.group(1)) |
| |
| defines = list(defines) |
| defines.sort() |
| return defines |
| |
| |
| RE_INCLUDE = re.compile('^.include "([^"]*)"$', flags=re.MULTILINE) |
| def flatten(f): |
| fdir = os.path.dirname(f) |
| data = open(f).read() |
| odata = [] |
| |
| oend = 0 |
| for m in RE_INCLUDE.finditer(data): |
| odata.append(data[oend:m.start(0)]) |
| incf = m.group('f') |
| if not incf: |
| raise IOError('invalid-include: {!r} in {!r}'.format(m.group(0), f)) |
| incpath = os.path.abspath(os.path.join(fdir, incf)) |
| if not os.path.isfile(incpath): |
| raise FileNotFoundError('invalid-include: {!r} in {!r}'.format(incpath, f)) |
| try: |
| odata.append(flatten(incpath)) |
| except IOError as e: |
| raise e.__class__(str(e) + '\n(included in {!r} byte:{})'.format(f, m.start(0))) |
| oend = m.end(0) |
| |
| odata.append(data[oend:]) |
| odata = ''.join(odata) |
| m = RE_INCLUDE.search(odata) |
| assert not m, m |
| return odata |
| |
| |
| def get_deps_for_device(pdk_path, version, device, corner, dependencies, provided_by): |
| devicedir = get_cell_directory(pdk_path, 'sky130_fd_pr', version, device) |
| print("Device", device, "found in", devicedir) |
| |
| if corner: |
| if device.endswith('__'+corner): |
| device = device[:-len('__'+corner)] |
| |
| spicefiles = [f for f in os.listdir(devicedir) if f.startswith(device) and f.endswith('.spice')] |
| spicefiles.sort() |
| |
| corner_spicefiles = [f for f in spicefiles if f.startswith(device+'__')] |
| |
| #if corner: |
| # topfile_prefix = device+'__'+corner+'.' |
| #else: |
| topfile_prefix = device+'.' |
| |
| top_spicefiles = [f for f in spicefiles if f.startswith(topfile_prefix)] |
| if len(top_spicefiles) != 1: |
| print(device, "Spice files:") |
| print(spicefiles) |
| print(device, "Top:") |
| pprint.pprint(top_spicefiles) |
| print(device, "Corner:") |
| pprint.pprint(corner_spicefiles) |
| |
| assert len(top_spicefiles) == 1, top_spicefiles |
| top_spice_file = top_spicefiles[0] |
| top_spice_path = os.path.abspath(os.path.join(devicedir, top_spice_file)) |
| top_spice_data = flatten(top_spice_path) |
| |
| provides = extract_defines(top_spice_data) |
| deps = extract_x(top_spice_data) |
| assert device in provides, "{} not found in {}\n---\n{}".format( |
| device, top_spice_file, pprint.pformat(provides)) |
| |
| for d in provides: |
| assert d not in provided_by, '{} in {} already provided by {}\n---\n{}'.format( |
| d, top_spicefile, provided_by[d], pprint.pformat(provided_by)) |
| provided_by[d] = top_spice_path |
| |
| assert d not in dependencies, device+'\n'+pprint.pformat(dependencies) |
| dependencies[d] = deps |
| |
| to_load = set() |
| for d in deps: |
| if d not in provided_by: |
| to_load.add(d) |
| |
| for d in to_load: |
| if d in provided_by: |
| continue |
| get_deps_for_device(pdk_path, version, d, corner, dependencies, provided_by) |
| |
| return |
| |
| |
| def flatten_deps_to_files(device, dependencies, provided_by): |
| files = [] |
| devices = [device] |
| while devices: |
| d = devices.pop(0) |
| assert d in provided_by, d+'\n'+pprint.pformat(provided_by) |
| if provided_by[d] in files: |
| continue |
| files.append(provided_by[d]) |
| assert d in dependencies, d+'\n'+pprint.pformat(dependencies) |
| devices.extend(dependencies[d]) |
| |
| files.reverse() |
| return files |
| |
| |
| |
| |
| def main(pdk_path, version='v0.20.1', device=None): |
| assert os.path.exists(pdk_path), pdk_path |
| prlib_path = os.path.join(pdk_path, "libraries", "sky130_fd_pr", version) |
| assert os.path.exists(prlib_path), prlib_path |
| |
| devicedir = get_cell_directory(pdk_path, 'sky130_fd_pr', version, device) |
| assert '__' not in devicedir, devicedir |
| |
| bits = device.split('__') |
| assert len(bits) > 1, (bits, device) |
| assert bits.pop(0) == 'sky130_fd_pr', (bits, device) |
| bits.pop(0) # Device directory |
| |
| if not bits: |
| corner = 'tt' |
| else: |
| corner = bits.pop(0) |
| |
| if device.endswith('__'+corner): |
| device = device[:-len('__'+corner)] |
| |
| print(devicedir, device, corner) |
| |
| dependencies = {} |
| provided_by = {} |
| get_deps_for_device(pdk_path, version, device, corner, dependencies, provided_by) |
| print() |
| print() |
| print(devicedir, device, corner) |
| pprint.pprint(dependencies) |
| pprint.pprint(provided_by) |
| print('------') |
| pprint.pprint(flatten_deps_to_files(device, dependencies, provided_by)) |
| |
| |
| #----------------------------------------------------------- |
| # Command-line entry point |
| #----------------------------------------------------------- |
| |
| if __name__ == "__main__": |
| import doctest |
| fails, _ = doctest.testmod() |
| if fails != 0: |
| sys.exit("Some test failed!") |
| |
| args = list(sys.argv) |
| args.pop(0) |
| |
| pdk_path = args.pop(0) |
| if args and args[0] and args[0][0] == 'v': |
| version = args.pop(0) |
| else: |
| version = 'v0.20.1' |
| |
| assert len(args) == 1, args |
| |
| sys.exit(main(pdk_path, version, device=args[0])) |
| |
| 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) |