| #!/usr/bin/env python3 |
| #------------------------------------------------------------------------------ |
| # |
| # run_spice_tests_new.py -- |
| # |
| # Run all ngspice tests, assuming an input file "devices.txt" containing, |
| # with one entry per line: |
| # |
| # <device_name> <device_type> <expected_value> |
| # |
| # where <device_type> is one of: mosfet, bipolar, capacitor, resistor, or diode. |
| # |
| #------------------------------------------------------------------------------ |
| |
| import os |
| import pprint |
| import re |
| import subprocess |
| import json |
| 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 |
| |
| import find_all_devices |
| import find_all_devices_new |
| |
| from plot_mosfet_iv import make_mosfet_iv_plot |
| from plot_bipolar_iv import make_bipolar_iv_plot |
| from plot_bipolar_beta import make_bipolar_beta_plot |
| from plot_diode_iv import make_diode_iv_plot |
| |
| #------------------------------------------------------------------------------ |
| # Enumerate capacitors that have additional shield pin connection |
| #------------------------------------------------------------------------------ |
| |
| four_pin_caps = [ |
| 'sky130_fd_pr__cap_vpp_03p9x03p9_m1m2_shieldl1_floatm3', |
| 'sky130_fd_pr__cap_vpp_04p4x04p6_l1m1m2_shieldpo_floatm3', |
| 'sky130_fd_pr__cap_vpp_04p4x04p6_m1m2m3_shieldl1m5_floatm4', |
| 'sky130_fd_pr__cap_vpp_06p8x06p1_l1m1m2m3_shieldpom4', |
| 'sky130_fd_pr__cap_vpp_06p8x06p1_m1m2m3_shieldl1m4', |
| 'sky130_fd_pr__cap_vpp_08p6x07p8_l1m1m2_shieldpo_floatm3', |
| 'sky130_fd_pr__cap_vpp_08p6x07p8_m1m2m3_shieldl1m5_floatm4', |
| 'sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv', |
| 'sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv__base', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2_shieldpom3', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3_shieldm4', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3_shieldpom4', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldm5', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldpom5', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldpom5_x', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_m1m2m3_shieldl1m5_floatm4', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_m1m2m3m4_shieldl1m5', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_m1m2m3m4_shieldm5', |
| 'sky130_fd_pr__cap_vpp_04p4x04p6_l1m1m2_shieldm3_floatpo', |
| 'sky130_fd_pr__cap_vpp_04p4x04p6_l1m1m2m3m4_shieldpom5', |
| 'sky130_fd_pr__cap_vpp_04p4x04p6_m1m2m3_shieldl1m5_floatm4_r', |
| 'sky130_fd_pr__cap_vpp_04p4x04p6_m1m2m3_shieldl1m5_floatm4_top', |
| 'sky130_fd_pr__cap_vpp_06p8x06p1_l1m1m2m3_shieldpom4_r', |
| 'sky130_fd_pr__cap_vpp_06p8x06p1_l1m1m2m3_shieldpom4_top', |
| 'sky130_fd_pr__cap_vpp_06p8x06p1_l1m1m2m3m4_shieldpo_floatm5', |
| 'sky130_fd_pr__cap_vpp_06p8x06p1_m1m2m3_shieldl1m4_r', |
| 'sky130_fd_pr__cap_vpp_06p8x06p1_m1m2m3_shieldl1m4_top', |
| 'sky130_fd_pr__cap_vpp_08p6x07p8_m1m2m3_shieldl1m5_floatm4_r', |
| 'sky130_fd_pr__cap_vpp_08p6x07p8_m1m2m3_shieldl1m5_floatm4_top', |
| 'sky130_fd_pr__cap_vpp_08p6x07p8_m1m2m3m4_shieldpom5', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2_shieldpom3_r', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3_shieldm4_top', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3_shieldpom4_top', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldm5_r', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldm5_top', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldpom5_m5pullin', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldpom5_r', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldpom5_top', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldpom5_x6', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldpom5_x7', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldpom5_x8', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldpom5_x9', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_l1m1m2m3m4_shieldpom5_xtop', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_m1m2m3_shieldl1m5_floatm4_top', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_m1m2m3m4_shieldl1m5_r', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_m1m2m3m4_shieldl1m5_top', |
| 'sky130_fd_pr__cap_vpp_11p5x11p7_m1m2m3m4_shieldm5_r' |
| ] |
| |
| #------------------------------------------------------------------------------ |
| # Enumerate nFETs that have additional p-well pin connection |
| #------------------------------------------------------------------------------ |
| |
| five_pin_fets = [ |
| 'sky130_fd_pr__nfet_20v0_iso', |
| 'sky130_fd_pr__nfet_20v0_nvt_iso', |
| 'sky130_fd_pr__nfet_20v0_reverse_iso' |
| ] |
| |
| #------------------------------------------------------------------------------ |
| # Specific handling of include files to work around limitations of the |
| # ad hoc methods used in find_all_devices.py. |
| #------------------------------------------------------------------------------ |
| |
| def includes_special_handling(devicename, corner, includes): |
| newinclist = [] |
| for incfile in includes: |
| # Pull the PDK path out of the include name |
| if '/models/' in incfile: |
| pdkbase_path = incfile.split('/models/')[0] |
| elif '/cells/' in incfile: |
| pdkbase_path = incfile.split('/cells/')[0] |
| else: |
| pdkbase_path = None |
| |
| if pdkbase_path: |
| modpath = pdkbase_path + '/models' |
| rcpath = modpath + '/r+c' |
| parampath = modpath + '/parameters' |
| cornerpath = modpath + '/corners/' + corner |
| |
| incbase = os.path.split(incfile)[1] |
| if incbase == 'sky130_fd_pr__model__cap_vpp.model.spice': |
| # Insert additional file. NOTE: Currently there is no way to pass |
| # a BEOL corner to this routine. |
| newinclist.append(rcpath + '/res_typical__cap_typical__lin.spice') |
| newinclist.append(rcpath + '/res_typical__cap_typical.spice') |
| elif incbase == 'sky130_fd_pr__model__cap_var.model.spice': |
| newinclist.append(rcpath + '/res_typical__cap_typical__lin.spice') |
| newinclist.append(rcpath + '/res_typical__cap_typical.spice') |
| newinclist.append(cornerpath + '/nonfet.spice') |
| elif incbase == corner + '.spice': |
| newinclist.append(rcpath + '/res_typical__cap_typical__lin.spice') |
| newinclist.append(rcpath + '/res_typical__cap_typical.spice') |
| elif '__npn_11v0' in devicename: |
| newinclist.append(modpath + '/sky130.lib.spice') |
| elif 'model__diode_pd2nw_11v0' in incbase: |
| newinclist.append(pdkbase_path + '/cells/pfet_g5v0d10v5/sky130_fd_pr__pfet_g5v0d10v5__' + corner + '.corner.spice') |
| elif 'model__diode_pw2nd_11v0' in incbase: |
| newinclist.append(modpath + '/sky130.lib.spice') |
| elif 'model__parasitic__diodes_pw2dn' in incbase: |
| newinclist.append(modpath + '/sky130.lib.spice') |
| elif '__npn_05v5__' in incbase: |
| newinclist.append(modpath + '/corners/' + corner + '/nonfet.spice') |
| elif '__pnp_05v5_W0' in devicename: |
| newinclist.append(modpath + '/corners/' + corner + '/nonfet.spice') |
| elif '__pnp_05v5_W3' in devicename: |
| newinclist.append(modpath + '/corners/' + corner + '.spice') |
| elif '__nfet_20v0_nvt_iso' in devicename: |
| newinclist.append(modpath + '/sky130.lib.spice') |
| newinclist.append(pdkbase_path + '/cells/nfet_20v0/sky130_fd_pr__nfet_20v0__tt_discrete.corner.spice') |
| |
| newinclist.append(incfile) |
| |
| return newinclist |
| |
| #------------------------------------------------------------------------------ |
| # Run ngspice on the indicated input file in the "results" directory. |
| #------------------------------------------------------------------------------ |
| |
| TIMEOUT = 360 |
| |
| def runspice(scriptpath, resultname, devicename, expectedval): |
| |
| scriptdir = os.path.dirname(scriptpath) |
| scriptfile = os.path.basename(scriptpath) |
| scriptbase, scriptext = scriptfile.split('.', 1) |
| |
| stdout_file = os.path.join(scriptdir, scriptbase+'.stdout') |
| stderr_file = os.path.join(scriptdir, scriptbase+'.stderr') |
| |
| stdout_fobj = open(stdout_file, 'w') |
| stderr_fobj = open(stderr_file, 'w') |
| |
| print('Running ngspice on', scriptfile, "(in %s)" % scriptdir) |
| with subprocess.Popen( |
| ['ngspice', scriptpath], |
| stdin= subprocess.DEVNULL, |
| stdout = stdout_fobj, |
| stderr = stderr_fobj, |
| cwd = scriptdir, |
| universal_newlines = True, |
| close_fds=True, |
| ) as sproc: |
| |
| stdout_fobj.close() |
| stderr_fobj.close() |
| |
| try: |
| sproc_retcode = sproc.wait(timeout=TIMEOUT) |
| except subprocess.TimeoutExpired as e: |
| print("WARNING: ngspice on %s timed out!" % scriptpath) |
| # Give ngspice 5 seconds to die on SIGTERM before sending SIGKILL |
| sproc.terminate() |
| try: |
| sproc_retcode = sproc.wait(timeout=5) |
| except subprocess.TimeoutExpired as e: |
| sproc.kill() |
| sproc_retcode = sproc.poll() |
| assert sproc_retcode is not None, str(sproc)+" won't terminate!?" |
| |
| stdout = open(stdout_file).read().splitlines(True) |
| stderr = open(stderr_file).read().splitlines(True) |
| |
| if True: |
| found = True if resultname == 'none' else False |
| valueline = False |
| returncode = 0 |
| reason = 'Success' |
| |
| for line in stdout: |
| if line.strip('\n').strip() == '': |
| continue |
| print('Diagnostic: ngspice output line is "' + line.strip('\n').strip() + '"') |
| if valueline: |
| try: |
| rvaluestring = line.split('=')[1].strip() |
| except: |
| rvaluestring = line.strip('\n').strip() |
| if rvaluestring != expectedval: |
| if expectedval == 'none': |
| returncode = rvaluestring |
| else: |
| print('Result error: Expected "' + resultname + '" with value "' + expectedval + '" but got ' + rvaluestring) |
| reason = 'Expected "' + resultname + '" with value "' + expectedval + '" but got ' + rvaluestring |
| returncode = -1 |
| valueline = False |
| |
| elif resultname != 'none' and resultname in line: |
| found = True |
| valueline = True |
| |
| if stderr: |
| errors = False |
| messages = [] |
| for line in stderr: |
| if len(line.strip()) > 0: |
| messages.append(line) |
| if 'error' in line.lower(): |
| errors = True |
| if errors: |
| print('Execution error: Ngspice returned error messages:') |
| # Print errors. Also try to pull the most relevant error message. |
| for message in messages: |
| print(message) |
| if 'find model' in message.lower() or 'undefined' in message.lower() or 'unknown' in message.lower() or 'valid model' in message.lower() or 'many parameters' in message.lower() or 'few parameters' in message.lower(): |
| reason = message |
| if reason == 'Success': |
| for message in messages: |
| if 'error' in message.lower(): |
| reason = message |
| if reason == 'Success': |
| reason = message[0] |
| returncode = -1 |
| else: |
| print('Ngspice warnings:') |
| for message in messages: |
| print(message) |
| return_code = sproc_retcode |
| if return_code != 0: |
| print('Execution error: Ngspice exited with error code ' + str(return_code)) |
| if reason == 'Success': |
| reason = 'Ngspice exited with error code ' + str(return_code) |
| return (-2, reason) |
| |
| if found: |
| return (returncode, reason) |
| else: |
| print('Result error: Expected result name ' + resultname + ' but did not receive it.') |
| reason = 'Expected result name ' + resultname + ' but did not receive it.' |
| return (-1, reason) |
| |
| #------------------------------------------------------------------------------ |
| # Read ".spice.in" file, replace DEVICENAME string with the device name, |
| # PDKVERSION with the version (v0.20.1), and CORNER with the simulation |
| # corner (tt), and write out as ".spice" file in "results" directory. |
| #------------------------------------------------------------------------------ |
| |
| def relpath(a, b): |
| """ |
| >>> relpath("a/c/1", "b/c") |
| "1" |
| |
| >>> relpath("a/c/2", "a/b") |
| "../c/2" |
| |
| >>> relpath( |
| ... "/ssd/gob/foss-eda-tools/skywater-pdk-scratch-new/libraries/sky130_fd_pr/v0.20.1/cells/rf_pfet_01v8/tests/sky130_fd_pr__rf_pfet_01v8_aF02W0p84L0p15__pfet_vth.spice", |
| ... "/ssd/gob/foss-eda-tools/skywater-pdk-scratch-new/skywater-pdk/libraries/sky130_fd_pr/v0.20.1/cells/rf_pfet_01v8/tests", |
| ... ) |
| |
| """ |
| a = os.path.abspath(a).split('/') |
| b = os.path.abspath(b).split('/') |
| |
| c = [] |
| while a[0] == b[0]: |
| c = a.pop(0) |
| b.pop(0) |
| |
| ac = a[len(c):] |
| bc = a[len(c):] |
| |
| return os.path.join(['..']*len(bc)+ac) |
| |
| |
| def genspice(outdir, scripttemplate, devicename, includes, corner): |
| print('Generating simulation netlist for device ' + devicename) |
| |
| templatefile = os.path.join(__dir__, scripttemplate+'.in') |
| assert os.path.exists(templatefile), templatefile |
| with open(templatefile) as ifile: |
| spicelines = ifile.read().splitlines() |
| |
| # Some parameters to pass to the output |
| # Default works for low-voltage FET devices |
| # (More are needed---some are being shadowed by other errors) |
| |
| if devicename == 'sky130_fd_pr__esd_nfet_01v8': |
| params = 'W=20.35 L=0.165 M=1' |
| elif devicename == 'sky130_fd_pr__esd_nfet_05v0_nvt': |
| params = 'W=10 L=2 M=1' |
| elif devicename == 'sky130_fd_pr__nfet_05v0_nvt': |
| params = 'W=10 L=2 M=1' |
| elif devicename == 'sky130_fd_pr__nfet_03v3_nvt': |
| params = 'W=10 L=0.5 M=1' |
| elif devicename == 'sky130_fd_pr__esd_nfet_03v3_nvt': |
| params = 'W=10 L=0.5 M=1' |
| elif devicename == 'sky130_fd_pr__esd_pfet_g5v0d10v5': |
| params = 'W=14.5 L=0.55 M=1' |
| elif devicename == 'sky130_fd_pr__esd_nfet_g5v0d10v5': |
| params = 'W=17.5 L=0.55 M=1' |
| elif devicename == 'sky130_fd_pr__pfet_01v8_mvt': |
| params = 'W=1.68 L=0.15 M=1' |
| elif devicename == 'sky130_fd_pr__pfet_01v8_lvt': |
| params = 'W=1.0 L=1.0 M=1' |
| elif devicename.startswith('sky130_fd_pr__nfet_g5v0d16v0'): |
| params = 'W=20 L=0.7 M=1' |
| elif devicename.startswith('sky130_fd_pr__pfet_g5v0d16v0'): |
| params = 'W=5 L=0.66 M=1' |
| elif devicename == 'sky130_fd_pr__pfet_g5v0d16v0': |
| params = 'W=20 L=0.8 M=1' |
| elif devicename == 'sky130_fd_pr__nfet_g5v0d16v0': |
| params = 'W=10 L=0.5 M=1' |
| elif devicename == 'sky130_fd_pr__special_nfet_pass_lvt': |
| params = 'W=0.3 L=0.15 M=1' |
| elif devicename == 'sky130_fd_pr__special_nfet_pass': |
| params = 'W=0.14 L=0.15 M=1' |
| elif devicename == 'sky130_fd_pr__special_pfet_pass': |
| params = 'W=0.14 L=0.15 M=1' |
| elif devicename == 'sky130_fd_pr__special_nfet_pass_flash': |
| params = 'W=0.45 L=0.15 M=1' |
| elif devicename == 'sky130_fd_pr__special_nfet_latch': |
| params = 'W=0.21 L=0.15 M=1' |
| elif devicename.startswith('sky130_fd_pr__nfet_20v0_nvt'): |
| params = 'W=20 L=1 M=1' |
| |
| # sky130_fd_pr__pfet_01v8 |
| elif devicename[-1] == 'v': |
| params = 'W=3.0 L=0.15 M=1' |
| |
| # sky130_fd_pr__rf_pfet_01v8_aF02W0p84L0p15 -- no params |
| else: |
| params = '' |
| |
| # More handling of subcircuits with partial parameter definitions |
| # and invalid defaults: |
| |
| if 'bM' in devicename and not 'W' in devicename and not 'L' in devicename: |
| if '01v8' in devicename: |
| params = 'W=1.65 L=0.15' |
| elif '10v5' in devicename: |
| if 'nfet_g5v0d10v5_b' in devicename: |
| # The 5V nFET for some reason is characterized at 3.01um |
| params = 'W=3.01 L=0.5' |
| else: |
| params = 'W=3.0 L=0.5' |
| elif 'bM' in devicename and 'W' in devicename and not 'L' in devicename: |
| if '01v8' in devicename: |
| params = 'L=0.15' |
| elif '10v5' in devicename: |
| params = 'L=0.5' |
| |
| foutpath = os.path.abspath(os.path.join(outdir, 'tests', devicename + '__' + scripttemplate)) |
| foutdir = os.path.dirname(foutpath) |
| os.makedirs(foutdir, exist_ok=True) |
| |
| includelines = [] |
| for incfile in includes: |
| relincfile = os.path.relpath(incfile, foutdir) |
| if '.lib.spice' in incfile: |
| includelines.append('.lib "{}" {}'.format(relincfile, corner)) |
| else: |
| includelines.append('.include "{}"'.format(relincfile)) |
| |
| outlines = [] |
| for line in spicelines: |
| newline = line.replace('INCLUDELINES', '\n'.join(includelines)) |
| newline = newline.replace('DEVICENAME', devicename) |
| newline = newline.replace('CORNER', corner) |
| newline = newline.replace('PARAMS', params) |
| outlines.append(newline) |
| |
| with open(foutpath, 'w') as ofile: |
| for line in outlines: |
| print(line, file=ofile) |
| |
| return foutpath |
| |
| #------------------------------------------------------------------------------ |
| # Run a spice simulation (or two) for the device specified in 'line' (obtained |
| # from the list of devices and expected values). |
| #------------------------------------------------------------------------------ |
| |
| def do_for_device(pdk_path, version, devicedir, devicename, corner): |
| |
| passes = [] |
| fails = [] |
| ignores = [] |
| baseline_run = False |
| baseline_entries = [] |
| reasons = {} |
| |
| # NOTE: When a line in the device list does not have an entry |
| # for expected value, then this is a baseline run, and will |
| # dump the device name and output measured to a file |
| # 'devices_baseline.txt' |
| |
| baseline_run = True |
| |
| # Determine the device type from the name |
| # (to do: Pull additional information about the device from the name) |
| # (also to do: separate nmos/pmos testbenches and pnp/npn testbenches) |
| |
| # Inductors |
| # sky130_fd_pr__ind.* |
| if "__ind" in devicename: |
| devicetype = 'inductor' |
| |
| # Capacitors |
| # sky130_fd_pr__cap_mim.* |
| # sky130_fd_pr__cap.* |
| elif "__cap_mim" in devicename: |
| devicetype = 'mimcap' |
| elif "__cap" in devicename: |
| devicetype = 'capacitor' |
| |
| # Diodes (as subcircuits) |
| # sky130_fd_pr__esd_rf_diode.* |
| # Diodes (as SPICE primitive devices) |
| # sky130_fd_pr__diode.* |
| elif "_diode" in devicename: |
| devicetype = 'diode' |
| |
| # MOSFETs (n-type) |
| # sky130_fd_pr__esd_nfet.* |
| # sky130_fd_pr__nfet.* |
| # sky130_fd_pr__rf_nfet.* |
| # sky130_fd_pr__special_nfet.* |
| elif "_nfet" in devicename: |
| devicetype = 'nfet' |
| |
| # MOSFETs (p-type) |
| # sky130_fd_pr__esd_pfet.* |
| # sky130_fd_pr__pfet.* |
| # sky130_fd_pr__rf_pfet.* |
| # sky130_fd_pr__special_pfet.* |
| elif "_pfet" in devicename: |
| devicetype = 'pfet' |
| |
| # Bipolars (NPN) |
| # sky130_fd_pr__rf_npn.* |
| # sky130_fd_pr__npn.* |
| elif "_npn" in devicename: |
| devicetype = 'npn' |
| # Bipolars (PNP) |
| # sky130_fd_pr__pnp.* |
| # sky130_fd_pr__rf_pnp.* |
| elif "_pnp" in devicename: |
| devicetype = 'pnp' |
| |
| # Resistors |
| # sky130_fd_pr__res.* |
| elif "_res" in devicename: |
| devicetype = 'resistor' |
| |
| else: |
| raise SystemError('Unknown device:'+devicename) |
| |
| # Check list of devices for additional sub-types |
| if devicetype == 'nfet': |
| if devicename in five_pin_fets: |
| devicetype = 'nfet5term' |
| |
| elif devicetype == 'capacitor': |
| if devicename in four_pin_caps: |
| devicetype = 'capacitor4pin' |
| |
| #deps = {} |
| #provided_by = {} |
| #find_all_devices_new.get_deps_for_device(pdk_path, version, devicename, corner, deps, provided_by) |
| #assert devicename in provided_by, (devicename+'\n'+pprint.pformat(provided_by)) |
| #includes = find_all_devices_new.flatten_deps_to_files(devicename, deps, provided_by) |
| top_dir = os.path.join(pdk_path, "skywater-pdk", "libraries", "sky130_fd_pr", version) |
| assert os.path.exists(top_dir), top_dir |
| includes = find_all_devices.do_find_all_devices(top_dir, None, devicename, feol=corner) |
| includes = [os.path.abspath(i) for i in includes] |
| |
| # Special handling to include files that need to be included but are not |
| # handled properly by the various ad-hoc methods used in find_all_devices.py |
| includes = includes_special_handling(devicename, corner, includes) |
| |
| print() |
| print("Includes needed:") |
| pprint.pprint(includes) |
| print() |
| |
| try: |
| print('Diagnostic: Determined device ' + devicename + ' to be type ' + devicetype) |
| except: |
| print('Unknown device type for device name ' + devicename + '. Cannot simulate.') |
| reasons[devicename] = 'Unknown device type for device name ' + devicename + '. Cannot simulate.' |
| fails.append(devicename) |
| return (passes, fails, ignores, baseline_entries, reasons) |
| |
| # Devices for which no include was found count as "ignores" |
| if includes == []: |
| ignores.append(devicename) |
| devicetype = 'ignored' |
| |
| if baseline_run: |
| expectedval = 'none' |
| |
| if devicetype == 'nfet' or devicetype == 'nfet5term' or devicetype == 'pfet': |
| spicefile = genspice(devicedir, devicetype + '_vth.spice', devicename, includes, corner) |
| (result, reason) = runspice(spicefile, 'threshold voltage', devicename, expectedval) |
| reasons[devicename] = reason |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes.append(devicename) |
| elif result != 0: |
| fails.append(devicename) |
| else: |
| passes.append(devicename) |
| if result != -2: |
| make_mosfet_iv_plot(devicedir + '/tests', devicename, version, corner) |
| |
| elif devicetype == 'npn' or devicetype == 'pnp': |
| spicefile = genspice(devicedir, devicetype + '.spice', devicename, includes, corner) |
| (result, reason) = runspice(spicefile, 'maximum beta', devicename, expectedval) |
| reasons[devicename] = reason |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes.append(devicename) |
| elif result != 0: |
| fails.append(devicename) |
| else: |
| passes.append(devicename) |
| if result != -2: |
| make_bipolar_iv_plot(devicedir + '/tests', devicename, version, corner) |
| make_bipolar_beta_plot(devicedir + '/tests', devicename, version, corner) |
| |
| elif devicetype == 'mimcap': |
| spicefile = genspice(devicedir, 'mimcap_test.spice', devicename, includes, corner) |
| (result, reason) = runspice(spicefile, 'capacitance', devicename, expectedval) |
| reasons[devicename] = reason |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes.append(devicename) |
| elif result != 0: |
| fails.append(devicename) |
| else: |
| passes.append(devicename) |
| |
| elif devicetype == 'capacitor': |
| spicefile = genspice(devicedir, 'capval_test.spice', devicename, includes, corner) |
| (result, reason) = runspice(spicefile, 'capacitance', devicename, expectedval) |
| reasons[devicename] = reason |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes.append(devicename) |
| elif result != 0: |
| fails.append(devicename) |
| else: |
| passes.append(devicename) |
| |
| elif devicetype == 'capacitor4pin': |
| spicefile = genspice(devicedir, 'cap4termval_test.spice', devicename, includes, corner) |
| (result, reason) = runspice(spicefile, 'capacitance', devicename, expectedval) |
| reasons[devicename] = reason |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes.append(devicename) |
| elif result != 0: |
| fails.append(devicename) |
| else: |
| passes.append(devicename) |
| |
| elif devicetype == 'inductor': |
| spicefile = genspice(devicedir, 'inductor_test.spice', devicename, includes, corner) |
| (result, reason) = runspice(spicefile, 'inductance', devicename, expectedval) |
| reasons[devicename] = reason |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes.append(devicename) |
| elif result != 0: |
| fails.append(devicename) |
| else: |
| passes.append(devicename) |
| |
| elif devicetype == 'resistor': |
| spicefile = genspice(devicedir, 'resval_test.spice', devicename, includes, corner) |
| (result, reason) = runspice(spicefile, 'resistance', devicename, expectedval) |
| reasons[devicename] = reason |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes.append(devicename) |
| elif result != 0: |
| fails.append(devicename) |
| else: |
| passes.append(devicename) |
| |
| elif devicetype == 'diode' or devicetype == 'diode_dev': |
| spicefile = genspice(devicedir, devicetype + '_vth.spice', devicename, includes, corner) |
| (result, reason) = runspice(spicefile, 'threshold voltage', devicename, expectedval) |
| reasons[devicename] = reason |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes.append(devicename) |
| elif result != 0: |
| fails.append(devicename) |
| else: |
| passes.append(devicename) |
| if result != -2: |
| make_diode_iv_plot(devicedir + '/tests', devicename, version, corner) |
| |
| output = (passes, fails, ignores, baseline_entries, reasons) |
| |
| bsim4v5_out = os.path.abspath(os.path.join(devicedir, 'tests', 'bsim4v5.out')) |
| if os.path.exists(bsim4v5_out): |
| os.unlink(bsim4v5_out) |
| |
| foutpath = os.path.abspath(os.path.join(devicedir, 'tests', '%s_results.json' % devicename)) |
| with open(foutpath, 'w') as f: |
| json.dump(dict(zip(('passes', 'fails', 'ignores', 'baseline_entries', 'reasons'), output)), f, sort_keys=True, indent=" ") |
| return output |
| |
| #------------------------------------------------------------------------------ |
| # Main script starts here (no arguments, at least for now) |
| #------------------------------------------------------------------------------ |
| |
| def main(pdk_path, version, device): |
| 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) |
| |
| bits = device.split('__') |
| assert len(bits) > 1, (bits, device) |
| assert bits.pop(0) == 'sky130_fd_pr', (bits, device) |
| ddir = os.path.join(prlib_path, "cells", bits.pop(0)) |
| |
| if bits: |
| corner = bits.pop(0) |
| else: |
| corner = 'tt' |
| |
| # One-off problem: Some things pop up in the "corner" field that are not |
| # corners. . . Not sure that this is the best handling, but at least it |
| # produces a valid result. |
| if corner == 'subcell' or corner == 'base' or corner == 'parasitic': |
| corner = 'tt' |
| |
| if device.endswith('__'+corner): |
| device = device[:-len('__'+corner)] |
| |
| passes, fails, ignores, baseline_entries, reasons = do_for_device( |
| pdk_path, version, devicedir, device, corner) |
| if passes: |
| print("Pass:", passes) |
| if fails: |
| print("Fails:", fails) |
| if ignores: |
| print("Ignores:", fails) |
| if baseline_entries: |
| print("Baseline Entries:", baseline_entries) |
| if reasons: |
| print("Reasons:", reasons) |
| |
| return len(fails) |
| |
| |
| |
| if __name__ == "__main__": |
| 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, args[0])) |