blob: 71c27c51a0cdb4059cf0f2da6985f2d9e88cf1cc [file] [log] [blame]
Tim Edwards956858b2020-08-27 22:40:32 -04001#!/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
11import re
12import os
13import sys
14
15#-----------------------------------------------------------
16# Find all models in the model directory path, recursively
17#-----------------------------------------------------------
18
19def 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
36def 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
57def 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
100def 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
135def 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
204def 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
285def 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
459if __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)