Tim Edwards | 956858b | 2020-08-27 22:40:32 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | #----------------------------------------------------------- |
| 3 | # |
| 4 | # Find all devices which have subcircuit definitions in the path |
| 5 | # skywater-pdk/libraries/sky130_fd_pr/VERSION/cells/. List all of |
| 6 | # these devices. Then find all paths from the directory models/ |
| 7 | # that will read the subcircuit definition through a hierarchical |
| 8 | # series of includes. |
| 9 | #----------------------------------------------------------- |
| 10 | |
| 11 | import re |
| 12 | import os |
| 13 | import sys |
| 14 | |
| 15 | #----------------------------------------------------------- |
| 16 | # Find all models in the model directory path, recursively |
| 17 | #----------------------------------------------------------- |
| 18 | |
| 19 | def addmodels(modelpath): |
| 20 | modelfmts = os.listdir(modelpath) |
| 21 | files_to_parse = [] |
| 22 | for modelfmt in modelfmts: |
| 23 | if os.path.isdir(modelpath + '/' + modelfmt): |
| 24 | files_to_parse.extend(addmodels(modelpath + '/' + modelfmt)) |
| 25 | else: |
| 26 | fmtext = os.path.splitext(modelfmt)[1] |
| 27 | if fmtext == '.spice': |
| 28 | files_to_parse.append(modelpath + '/' + modelfmt) |
| 29 | |
| 30 | return files_to_parse |
| 31 | |
| 32 | #----------------------------------------------------------- |
| 33 | # Find the device name in a SPICE "X" line |
| 34 | #----------------------------------------------------------- |
| 35 | |
| 36 | def get_device_name(line): |
| 37 | # The instance name has already been parsed out of the line, and |
| 38 | # all continuation lines have been added, so this routine finds |
| 39 | # the last keyword that is not a parameter (i.e., does not contain |
| 40 | # '='). |
| 41 | |
| 42 | tokens = line.split() |
| 43 | for token in tokens: |
| 44 | if '=' in token: |
| 45 | break |
| 46 | else: |
| 47 | devname = token |
| 48 | |
| 49 | return devname |
| 50 | |
| 51 | #----------------------------------------------------------- |
| 52 | # Pick the file from the list that is appropriate for the |
| 53 | # choice of either FEOL or BEOL corner. Mostly ad-hoc rules |
| 54 | # based on the known file names. |
| 55 | #----------------------------------------------------------- |
| 56 | |
| 57 | def choose_preferred(inclist, feol, beol, notop, debug): |
| 58 | |
| 59 | try: |
| 60 | # The top-level file 'sky130.lib.spice' is always preferred |
| 61 | incfile = next(item for item in inclist if os.path.split(item)[1] == 'sky130.lib.spice') |
| 62 | except: |
| 63 | # (1) Sort list by the length of the root name of the file |
| 64 | inclist = sorted(inclist, key=lambda x: len(os.path.splitext(os.path.split(x)[1])[0])) |
| 65 | |
| 66 | # (2) Sort list by depth of directory hierarchy |
| 67 | inclist = sorted(inclist, key=lambda x: len(x.split('/'))) |
| 68 | |
| 69 | for incfile in inclist: |
| 70 | incname = os.path.split(incfile)[1] |
| 71 | if debug: |
| 72 | print(' choose preferred, checking: "' + incname + '"') |
| 73 | |
| 74 | elif incname == 'custom.spice': |
| 75 | # Ignore "custom.spice" (example file, unused) |
| 76 | continue |
| 77 | elif notop and incname.startswith('correl'): |
| 78 | # Ignore "correl1.spice", etc., if "-notop" option chosen |
| 79 | continue |
| 80 | |
| 81 | elif feol in incname: |
| 82 | break |
| 83 | elif 't' in beol and 'typical' in incname: |
| 84 | break |
| 85 | elif 'h' in beol and 'high' in incname: |
| 86 | break; |
| 87 | elif 'l' in beol and 'low' in incname: |
| 88 | break; |
| 89 | |
| 90 | if debug: |
| 91 | incname = os.path.split(incfile)[1] |
| 92 | print(' choose_preferred: chose ' + incfile + ' (' + incname + ')') |
| 93 | return incfile |
| 94 | |
| 95 | #--------------------------------------------------------------------- |
| 96 | # Sort files with the subcircuit by relevance. Files are considered |
| 97 | # in the order ".model.spice", ".pm3.spice", and ".spice". |
| 98 | #--------------------------------------------------------------------- |
| 99 | |
| 100 | def preferred_order(subfiles, feol): |
| 101 | ordfiles = [] |
| 102 | feolstr = '__' + feol |
| 103 | |
| 104 | # Sort by length first, so shorter ones, e.g., without "leak" or |
| 105 | # "discrete", end up at the front of the list. |
| 106 | ordlist = sorted(subfiles, key=len) |
| 107 | |
| 108 | for file in ordlist[:]: |
| 109 | if file.endswith('.corner.spice') and feolstr in file: |
| 110 | ordfiles.append(file) |
| 111 | ordlist.remove(file) |
| 112 | |
| 113 | for file in ordlist[:]: |
| 114 | if file.endswith('.model.spice'): |
| 115 | ordfiles.append(file) |
| 116 | ordlist.remove(file) |
| 117 | |
| 118 | for file in ordlist[:]: |
| 119 | if file.endswith('.pm3.spice') and feolstr in file: |
| 120 | ordfiles.append(file) |
| 121 | ordlist.remove(file) |
| 122 | |
| 123 | for file in ordlist[:]: |
| 124 | if file.endswith('.pm3.spice'): |
| 125 | ordfiles.append(file) |
| 126 | ordlist.remove(file) |
| 127 | |
| 128 | ordfiles.extend(ordlist) |
| 129 | return ordfiles |
| 130 | |
| 131 | #----------------------------------------------------------- |
| 132 | # Find the appropriate file to include to handle this device |
| 133 | #----------------------------------------------------------- |
| 134 | |
| 135 | def check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug): |
| 136 | # subfiles = list of files that define this subcircuit. |
| 137 | # includedict = files that include the dictionary keyword file |
| 138 | # modfilesdict = files that are included by the dictionary keyword file |
| 139 | # feol = FEOL corner (fs, tt, ss, etc.) for transistors, diodes |
| 140 | # beol = BEOL corner (hl, tt, ll, etc.) for capacitors, resistors, inductors |
| 141 | |
| 142 | ordfiles = preferred_order(subfiles, feol) |
| 143 | if debug: |
| 144 | print('') |
| 145 | print('Check_device: Search order:') |
| 146 | for ordfile in ordfiles: |
| 147 | print(' ' + os.path.split(ordfile)[1]) |
| 148 | |
| 149 | # Find the proper include file to include this device. Attempt on all |
| 150 | # entries in ordfiles, and stop at the first one that returns a result. |
| 151 | # Assume that all files have unique names and point to the proper |
| 152 | # location, so that is is only necessary to look at the last path |
| 153 | # component. |
| 154 | |
| 155 | if debug: |
| 156 | print('\nClimb hierarchy of includes to the top:') |
| 157 | |
| 158 | for ordfile in ordfiles: |
| 159 | ordname = os.path.split(ordfile)[1] |
| 160 | if debug: |
| 161 | print(' (1) Search for "' + ordname + '"') |
| 162 | try: |
| 163 | inclist = includedict[ordname][1:] |
| 164 | except: |
| 165 | if debug: |
| 166 | print(' No include file found for "' + ordfile + '"') |
| 167 | print(' Sample entry:') |
| 168 | for key in includedict: |
| 169 | print(' ' + key + ': "' + str(includedict[key][1:]) + '"') |
| 170 | break |
| 171 | continue |
| 172 | else: |
| 173 | if debug: |
| 174 | print(' Starting list = ') |
| 175 | for item in inclist: |
| 176 | print(' ' + item) |
| 177 | |
| 178 | while True: |
| 179 | incfile = choose_preferred(inclist, feol, beol, notop, debug) |
| 180 | incname = os.path.split(incfile)[1] |
| 181 | if debug: |
| 182 | print(' (2) Search for "' + incname + '"') |
| 183 | try: |
| 184 | inclist = includedict[incname][1:] |
| 185 | except: |
| 186 | break |
| 187 | else: |
| 188 | if debug: |
| 189 | print(' Continuing list = ') |
| 190 | for item in inclist: |
| 191 | print(' ' + item) |
| 192 | |
| 193 | if debug: |
| 194 | print('Final top level include file is: "' + incfile + '"') |
| 195 | return incfile |
| 196 | |
| 197 | # Should only happen if subfiles is empty list |
| 198 | return None |
| 199 | |
| 200 | #----------------------------------------------------------- |
| 201 | # Find all cells and all models |
| 202 | #----------------------------------------------------------- |
| 203 | |
| 204 | def find_everything(pathtop): |
| 205 | cellspath = pathtop + '/cells' |
| 206 | modelspath = pathtop + '/models' |
| 207 | |
| 208 | allcells = os.listdir(cellspath) |
| 209 | |
| 210 | subcktrex = re.compile('\.subckt[ \t]+([^ \t]+)[ \t]+', re.IGNORECASE) |
| 211 | includerex = re.compile('\.include[ \t]+([^ \t]+)', re.IGNORECASE) |
| 212 | |
| 213 | filesdict = {} |
| 214 | subcktdict = {} |
| 215 | includedict = {} |
| 216 | modfilesdict = {} |
| 217 | |
| 218 | for cellfile in allcells: |
| 219 | cellpath = cellspath + '/' + cellfile |
| 220 | cellfmts = os.listdir(cellpath) |
| 221 | files_to_parse = [] |
| 222 | for cellfmt in cellfmts: |
| 223 | fmtext = os.path.splitext(cellfmt)[1] |
| 224 | if fmtext == '.spice': |
| 225 | files_to_parse.append(cellpath + '/' + cellfmt) |
| 226 | |
| 227 | for file in files_to_parse: |
| 228 | with open(file, 'r') as ifile: |
| 229 | spicelines = ifile.read().splitlines() |
| 230 | for line in spicelines: |
| 231 | smatch = subcktrex.match(line) |
| 232 | if smatch: |
| 233 | subname = smatch.group(1) |
| 234 | try: |
| 235 | subcktdict[subname].append(file) |
| 236 | except: |
| 237 | subcktdict[subname] = [file] |
| 238 | filetail = os.path.split(file)[1] |
| 239 | try: |
| 240 | filesdict[filetail].append(subname) |
| 241 | except: |
| 242 | filesdict[filetail] = [subname] |
| 243 | |
| 244 | files_to_parse = addmodels(modelspath) |
| 245 | files_to_parse.extend(addmodels(cellspath)) |
| 246 | |
| 247 | for file in files_to_parse: |
| 248 | # NOTE: Avoid problems with sonos directories using |
| 249 | # "tt.spice", which causes the include chain recursive |
| 250 | # loop to fail to exit. This is a one-off exception |
| 251 | # (hack alert) |
| 252 | if '_of_life' in file: |
| 253 | continue |
| 254 | |
| 255 | with open(file, 'r') as ifile: |
| 256 | spicelines = ifile.read().splitlines() |
| 257 | for line in spicelines: |
| 258 | imatch = includerex.match(line) |
| 259 | if imatch: |
| 260 | incname = imatch.group(1).strip('"') |
| 261 | inckey = os.path.split(incname)[1] |
| 262 | |
| 263 | try: |
| 264 | inclist = includedict[inckey] |
| 265 | except: |
| 266 | includedict[inckey] = [incname, file] |
| 267 | else: |
| 268 | if file not in inclist[1:]: |
| 269 | includedict[inckey].append(file) |
| 270 | filetail = os.path.split(file)[1] |
| 271 | try: |
| 272 | modlist = modfilesdict[filetail] |
| 273 | except: |
| 274 | modfilesdict[filetail] = [incname] |
| 275 | else: |
| 276 | if incname not in modlist: |
| 277 | modfilesdict[filetail].append(incname) |
| 278 | |
| 279 | return filesdict, subcktdict, includedict, modfilesdict |
| 280 | |
| 281 | #----------------------------------------------------------- |
| 282 | # Main application |
| 283 | #----------------------------------------------------------- |
| 284 | |
| 285 | def do_find_all_devices(pathtop, sourcefile, cellname=None, feol='tt', beol='tt', doall=False, notop=False, debug=False): |
| 286 | |
| 287 | (filesdict, subcktdict, includedict, modfilesdict) = find_everything(pathtop) |
| 288 | |
| 289 | if sourcefile: |
| 290 | # Parse the source file and find all 'X' records, and collect a list |
| 291 | # of all primitive devices used in the file by cross-checking against |
| 292 | # the dictionary of subcircuits. |
| 293 | |
| 294 | devrex = re.compile('x([^ \t]+)[ \t]+(.*)', re.IGNORECASE) |
| 295 | incfiles = [] |
| 296 | |
| 297 | with open(sourcefile, 'r') as ifile: |
| 298 | spicelines = ifile.read().splitlines() |
| 299 | |
| 300 | if debug: |
| 301 | print('Netlist file first line is "' + spicelines[0] + '"') |
| 302 | |
| 303 | isdev = False |
| 304 | for line in spicelines: |
| 305 | if line.startswith('*'): |
| 306 | continue |
| 307 | if line.strip() == '': |
| 308 | continue |
| 309 | elif line.startswith('+'): |
| 310 | if isdev: |
| 311 | rest += line[1:] |
| 312 | elif isdev: |
| 313 | devname = get_device_name(rest) |
| 314 | try: |
| 315 | subfiles = subcktdict[devname] |
| 316 | except: |
| 317 | pass |
| 318 | else: |
| 319 | incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) |
| 320 | if not incfile: |
| 321 | incfile = preferred_order(subfiles, feol)[0] |
| 322 | |
| 323 | if incfile: |
| 324 | if debug: |
| 325 | print('Device ' + devname + ': Include ' + incfile) |
| 326 | if incfile not in incfiles: |
| 327 | incfiles.append(incfile) |
| 328 | else: |
| 329 | print('Something went dreadfully wrong with device "' + devname + '"') |
| 330 | |
| 331 | isdev = False |
| 332 | |
| 333 | smatch = devrex.match(line) |
| 334 | if smatch: |
| 335 | instname = smatch.group(1) |
| 336 | rest = smatch.group(2) |
| 337 | isdev = True |
| 338 | elif isdev: |
| 339 | devname = get_device_name(rest) |
| 340 | try: |
| 341 | subfiles = subcktdict[devname] |
| 342 | except: |
| 343 | pass |
| 344 | else: |
| 345 | incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) |
| 346 | if not incfile: |
| 347 | incfile = preferred_order(subfiles, feol)[0] |
| 348 | |
| 349 | if incfile: |
| 350 | if debug: |
| 351 | print('Device "' + devname + '": Include "' + incfile + '"') |
| 352 | if incfile not in incfiles: |
| 353 | incfiles.append(incfile) |
| 354 | else: |
| 355 | print('Something went dreadfully wrong with device "' + devname + '"') |
| 356 | isdev = False |
| 357 | |
| 358 | # Return the .include lines needed |
| 359 | return incfiles |
| 360 | |
| 361 | elif cellname: |
| 362 | # Diagnostic: Given a cell name on the command line (with -cell=<name>), |
| 363 | # Run check_device() on the cell and report. |
| 364 | try: |
| 365 | subfiles = subcktdict[cellname] |
| 366 | except: |
| 367 | print('No cell "' + cellname + '" was found in the PDK files.') |
| 368 | sys.exit(1) |
| 369 | |
| 370 | incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) |
| 371 | if debug: |
| 372 | print('') |
| 373 | print('Report:') |
| 374 | print('') |
| 375 | print('Cell = "' + cellname + '"') |
| 376 | print('') |
| 377 | |
| 378 | bestfilepath = preferred_order(subfiles, feol)[0] |
| 379 | if bestfilepath.startswith(pathtop): |
| 380 | bestfile = bestfilepath[len(pathtop) + 1:] |
| 381 | print('Subcircuit defined in (from ' + pathtop + '/): "' + bestfile + '"') |
| 382 | |
| 383 | if debug: |
| 384 | print('') |
| 385 | print('Top level include: ') |
| 386 | |
| 387 | if incfile: |
| 388 | return [incfile] |
| 389 | else: |
| 390 | return [bestfilepath] |
| 391 | |
| 392 | elif doall: |
| 393 | allincludes = [] |
| 394 | for cellname in subcktdict: |
| 395 | |
| 396 | # Diagnostic: Given a cell name on the command line (with -cell=<name>), |
| 397 | # Run check_device() on the cell and report. |
| 398 | try: |
| 399 | subfiles = subcktdict[cellname] |
| 400 | except: |
| 401 | print('No cell "' + cellname + '" was found in the PDK files.') |
| 402 | continue |
| 403 | |
| 404 | incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) |
| 405 | print('Cell = "' + cellname + '"') |
| 406 | bestfilepath = preferred_order(subfiles, feol)[0] |
| 407 | if bestfilepath.startswith(pathtop): |
| 408 | bestfile = bestfilepath[len(pathtop) + 1:] |
| 409 | print(' Subcircuit: "' + os.path.split(bestfile)[1] + '"') |
| 410 | print(' Include: ', end='') |
| 411 | if incfile: |
| 412 | if incfile not in allincludes: |
| 413 | allincludes.append(incfile) |
| 414 | print('"' + incfile + '"') |
| 415 | else: |
| 416 | if bestfilepath not in allincludes: |
| 417 | allincludes.append(bestfilepath) |
| 418 | print('"' + bestfilepath + '"') |
| 419 | |
| 420 | print('') |
| 421 | print('Summary: All files to include:\n') |
| 422 | return allincludes |
| 423 | |
| 424 | else: |
| 425 | # No source file given, so just dump the lists of subcircuits, models, |
| 426 | # and files into four different output files. |
| 427 | |
| 428 | nsubs = 0 |
| 429 | with open('sublist.txt', 'w') as ofile: |
| 430 | for key in subcktdict: |
| 431 | nsubs += 1 |
| 432 | value = subcktdict[key] |
| 433 | print(key + ': ' + ', '.join(value), file=ofile) |
| 434 | |
| 435 | nfiles = 0 |
| 436 | with open('filelist.txt', 'w') as ofile: |
| 437 | for key in filesdict: |
| 438 | nfiles += 1 |
| 439 | value = filesdict[key] |
| 440 | print(key + ': ' + ', '.join(value), file=ofile) |
| 441 | |
| 442 | with open('inclist.txt', 'w') as ofile: |
| 443 | for key in includedict: |
| 444 | value = includedict[key] |
| 445 | print(key + '(' + value[0] + '): ' + ', '.join(value[1:]), file=ofile) |
| 446 | |
| 447 | with open('modfilelist.txt', 'w') as ofile: |
| 448 | for key in modfilesdict: |
| 449 | value = modfilesdict[key] |
| 450 | print(key + ': ' + ', '.join(value), file=ofile) |
| 451 | |
| 452 | print('Found ' + str(nsubs) + ' subcircuit definitions in ' + str(nfiles) + ' files.') |
| 453 | return [] |
| 454 | |
| 455 | #----------------------------------------------------------- |
| 456 | # Command-line entry point |
| 457 | #----------------------------------------------------------- |
| 458 | |
| 459 | if __name__ == "__main__": |
| 460 | |
| 461 | optionlist = [] |
| 462 | arguments = [] |
| 463 | |
| 464 | for option in sys.argv[1:]: |
| 465 | if option.find('-', 0) == 0: |
| 466 | optionlist.append(option) |
| 467 | else: |
| 468 | arguments.append(option) |
| 469 | |
| 470 | # Defaults: Set up for the most recent PDK version. |
| 471 | version = 'v0.20.1' |
| 472 | pathtop = '../../libraries/sky130_fd_pr/' + version |
| 473 | |
| 474 | # Default FEOL corner is "tt", and default BEOL corner is "tt" |
| 475 | feol = 'tt' |
| 476 | beol = 'tt' |
| 477 | cellname = None |
| 478 | debug = False |
| 479 | doall = False |
| 480 | notop = False |
| 481 | |
| 482 | # Override defaults from options |
| 483 | |
| 484 | for option in optionlist: |
| 485 | if option.startswith('-version'): |
| 486 | try: |
| 487 | version = option.split('=')[1] |
| 488 | except: |
| 489 | print('Option usage: -version=<versionstring>') |
| 490 | sys.exit(1) |
| 491 | elif option.startswith('-corner') or option.startswith('-feol'): |
| 492 | try: |
| 493 | feol = option.split('=')[1] |
| 494 | except: |
| 495 | print('Option usage: -feol=<corner_name>') |
| 496 | sys.exit(1) |
| 497 | elif option.startswith('-beol'): |
| 498 | try: |
| 499 | beol = option.split('=')[1] |
| 500 | except: |
| 501 | print('Option usage: -beol=<corner_name>') |
| 502 | sys.exit(1) |
| 503 | elif option.startswith('-cell'): |
| 504 | try: |
| 505 | cellname = option.split('=')[1] |
| 506 | except: |
| 507 | print('Option usage: -cell=<cell_name>') |
| 508 | sys.exit(1) |
| 509 | elif option == '-notop': |
| 510 | notop = True |
| 511 | elif option == '-all': |
| 512 | doall = True |
| 513 | elif option == '-debug': |
| 514 | debug = True |
| 515 | |
| 516 | # Parse "-pdkpath" after the others because it is dependent on any option |
| 517 | # "-version" passed on the command line. |
| 518 | |
| 519 | for option in optionlist: |
| 520 | if option.startswith('-pdkpath'): |
| 521 | try: |
| 522 | pathroot = option.split('=')[1] |
| 523 | except: |
| 524 | print('Option usage: -pdkpath=<pathname>') |
| 525 | sys.exit(1) |
| 526 | if not os.path.isdir(pathroot): |
| 527 | print('Cannot find PDK directory ' + pathroot) |
| 528 | sys.exit(1) |
| 529 | pathtop = pathroot + '/libraries/sky130_fd_pr/' + version |
| 530 | if not os.path.isdir(pathtop): |
| 531 | print('Cannot find primitive device directory ' + pathtop) |
| 532 | sys.exit(1) |
| 533 | |
| 534 | # To be done: Make this a useful routine that can insert one or more |
| 535 | # .include statements into a SPICE netlist. Should take any number of |
| 536 | # files on the arguments line and modify the files in place. |
| 537 | |
| 538 | if len(arguments) > 0: |
| 539 | sourcefile = arguments[0] |
| 540 | if not os.path.isfile(sourcefile): |
| 541 | print('Cannot read SPICE source file ' + sourcefile) |
| 542 | sys.exit(1) |
| 543 | else: |
| 544 | sourcefile = None |
| 545 | |
| 546 | if not os.path.isdir(pathtop): |
| 547 | print('Cannot find PDK path top level directory ' + pathtop) |
| 548 | sys.exit(1) |
| 549 | elif debug: |
| 550 | print('\nFinding everything in ' + pathtop + '.') |
| 551 | |
| 552 | incfiles = do_find_all_devices(pathtop, sourcefile, cellname, feol, beol, doall, notop, debug) |
| 553 | for incfile in incfiles: |
| 554 | if incfile.endswith('.lib.spice'): |
| 555 | print('.lib ' + incfile + ' ' + feol) |
| 556 | else: |
| 557 | print('.include "' + incfile + '"') |
| 558 | |
| 559 | sys.exit(0) |