| #!/usr/bin/env python3 |
| #------------------------------------------------------------------------------ |
| # |
| # run_spice_tests.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 re |
| import os |
| import sys |
| import subprocess |
| import multiprocessing |
| |
| import find_all_devices |
| |
| 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_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' |
| ] |
| |
| five_pin_fets = [ |
| 'sky130_fd_pr__nfet_20v0_iso', |
| 'sky130_fd_pr__nfet_20v0_nvt_iso', |
| 'sky130_fd_pr__nfet_20v0_reverse_iso' |
| ] |
| |
| #------------------------------------------------------------------------------ |
| # Run ngspice on the indicated input file in the "results" directory. |
| #------------------------------------------------------------------------------ |
| |
| def runspice(scriptname, resultname, devicename, expectedval): |
| print('Running ngspice on ' + scriptname) |
| with subprocess.Popen( |
| ['ngspice', scriptname], |
| stdin= subprocess.DEVNULL, |
| stdout = subprocess.PIPE, |
| stderr = subprocess.PIPE, |
| cwd = 'results', |
| universal_newlines = True) as sproc: |
| |
| stdout, stderr = sproc.communicate() |
| stdout = stdout.splitlines(True) |
| stderr = stderr.splitlines(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.wait() |
| 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 genspice(scriptname, devicename, version, corner): |
| print('Generating simulation netlist for device ' + devicename) |
| with open(scriptname + '.in', 'r') as ifile: |
| spicelines = ifile.read().splitlines() |
| |
| # Some parameters to pass to the output |
| # Default works for low-voltage FET devices |
| |
| params = 'W=3.0 L=0.15 M=1' |
| |
| 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' |
| |
| includelines = [] |
| pathtop = '../../libraries/sky130_fd_pr/' + version |
| print("Diagnostic: Running find_all_devices.do_find_all_devices()") |
| incfiles = find_all_devices.do_find_all_devices(pathtop, None, devicename, feol=corner) |
| for incfile in incfiles: |
| if '.lib.spice' in incfile: |
| includelines.append('.lib "../' + incfile + '" ' + corner) |
| else: |
| includelines.append('.include "../' + incfile + '"') |
| |
| outlines = [] |
| for line in spicelines: |
| newline = line.replace('INCLUDELINES', '\n'.join(includelines)) |
| newline = newline.replace('DEVICENAME', devicename) |
| newline = newline.replace('PDKVERSION', version) |
| newline = newline.replace('CORNER', corner) |
| newline = newline.replace('PARAMS', params) |
| outlines.append(newline) |
| |
| with open('results/' + devicename + '.spice', 'w') as ofile: |
| for line in outlines: |
| print(line, file=ofile) |
| |
| #------------------------------------------------------------------------------ |
| # 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(line): |
| passes = [] |
| fails = [] |
| baseline_run = False |
| baseline_entries = [] |
| reasons = {} |
| |
| devvals = line.split() |
| |
| # Capacitors |
| cap1rex = re.compile('sky130_fd_pr__cap_mim.*') |
| cap2rex = re.compile('sky130_fd_pr__cap.*') |
| # Diodes (as subcircuits) |
| diode1rex = re.compile('sky130_fd_pr__esd_rf_diode.*') |
| # Diodes (as SPICE primitive devices) |
| diode2rex = re.compile('sky130_fd_pr__diode.*') |
| # Inductors |
| indrex = re.compile('sky130_fd_pr__ind.*') |
| # Resistors |
| resrex = re.compile('sky130_fd_pr__res.*') |
| # MOSFETs (p-type) |
| pfet1rex = re.compile('sky130_fd_pr__esd_pfet.*') |
| pfet2rex = re.compile('sky130_fd_pr__pfet.*') |
| pfet3rex = re.compile('sky130_fd_pr__rf_pfet.*') |
| pfet4rex = re.compile('sky130_fd_pr__special_pfet.*') |
| # MOSFETs (n-type) |
| nfet1rex = re.compile('sky130_fd_pr__esd_nfet.*') |
| nfet2rex = re.compile('sky130_fd_pr__nfet.*') |
| nfet3rex = re.compile('sky130_fd_pr__rf_nfet.*') |
| nfet4rex = re.compile('sky130_fd_pr__special_nfet.*') |
| # Bipolars (PNP) |
| pnp1rex = re.compile('sky130_fd_pr__pnp.*') |
| pnp2rex = re.compile('sky130_fd_pr__rf_pnp.*') |
| # Bipolars (NPN) |
| npn1rex = re.compile('sky130_fd_pr__rf_npn.*') |
| npn2rex = re.compile('sky130_fd_pr__npn.*') |
| |
| devicename = devvals[0] |
| |
| # 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' |
| |
| try: |
| expectedval = devvals[1] |
| except: |
| baseline_run = True |
| else: |
| baseline_run = False |
| |
| # 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) |
| |
| if indrex.match(devicename): |
| devicetype = 'inductor' |
| elif cap1rex.match(devicename): |
| devicetype = 'mimcap' |
| elif cap2rex.match(devicename): |
| if devicename in four_pin_caps: |
| devicetype = 'capacitor4pin' |
| else: |
| devicetype = 'capacitor' |
| elif diode1rex.match(devicename): |
| devicetype = 'diode' |
| elif diode2rex.match(devicename): |
| devicetype = 'diode_dev' |
| elif nfet1rex.match(devicename): |
| devicetype = 'nfet' |
| elif nfet2rex.match(devicename): |
| devicetype = 'nfet' |
| elif nfet3rex.match(devicename): |
| devicetype = 'nfet' |
| elif nfet4rex.match(devicename): |
| devicetype = 'nfet' |
| elif pfet1rex.match(devicename): |
| devicetype = 'pfet' |
| elif pfet2rex.match(devicename): |
| devicetype = 'pfet' |
| elif pfet3rex.match(devicename): |
| devicetype = 'pfet' |
| elif pfet4rex.match(devicename): |
| devicetype = 'pfet' |
| elif pnp1rex.match(devicename): |
| devicetype = 'pnp' |
| elif pnp2rex.match(devicename): |
| devicetype = 'pnp' |
| elif npn1rex.match(devicename): |
| devicetype = 'npn' |
| elif npn2rex.match(devicename): |
| devicetype = 'npn' |
| elif resrex.match(devicename): |
| devicetype = 'resistor' |
| |
| if devicetype == 'nfet': |
| if devicename in five_pin_fets: |
| devicetype = 'nfet5term' |
| |
| 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, baseline_entries, reasons) |
| |
| if baseline_run: |
| expectedval = 'none' |
| |
| if devicetype == 'nfet' or devicetype == 'nfet5term' or devicetype == 'pfet': |
| genspice(devicetype + '_vth.spice', devicename, version, corner) |
| (result, reason) = runspice(devicename + '.spice', '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(devicename, version, corner) |
| |
| elif devicetype == 'npn' or devicetype == 'pnp': |
| genspice(devicetype + '_beta.spice', devicename, version, corner) |
| (result, reason) = runspice(devicename + '.spice', '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(devicename, version, corner) |
| make_bipolar_beta_plot(devicename, version, corner) |
| |
| elif devicetype == 'mimcap': |
| genspice('mimcap_test.spice', devicename, version, corner) |
| (result, reason) = runspice(devicename + '.spice', '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': |
| genspice('capval_test.spice', devicename, version, corner) |
| (result, reason) = runspice(devicename + '.spice', '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': |
| genspice('cap4termval_test.spice', devicename, version, corner) |
| (result, reason) = runspice(devicename + '.spice', '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 == 'resistor': |
| genspice('resval_test.spice', devicename, version, corner) |
| (result, reason) = runspice(devicename + '.spice', '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 == 'inductor': |
| genspice('inductor_test.spice', devicename, version, corner) |
| (result, reason) = runspice(devicename + '.spice', '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 == 'diode' or devicetype == 'diode_dev': |
| genspice(devicetype + '_vth.spice', devicename, version, corner) |
| (result, reason) = runspice(devicename + '.spice', '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(devicename, version, corner) |
| |
| return (passes, fails, baseline_entries, reasons) |
| |
| #------------------------------------------------------------------------------ |
| # Main script starts here (no arguments, at least for now) |
| #------------------------------------------------------------------------------ |
| |
| if __name__ == "__main__": |
| |
| if not os.path.exists('results'): |
| os.makedirs('results') |
| |
| # To do: Loop through version and corner. For now, fixed. |
| version = 'v0.20.1' |
| corner = 'tt' |
| |
| # Original method: List of devices was in file devices.txt. If an argument |
| # is passed to the program, then use it as the list of devices to process. |
| # To get the original behavior, use "run_spice_tests.py devices.txt". |
| |
| devicelist = [] |
| if len(sys.argv) > 1: |
| with open(sys.argv[1]) as ifile: |
| ilines = ifile.read().splitlines() |
| for line in ilines: |
| if not line.strip().startswith('#'): |
| devicelist.append(line.strip()) |
| else: |
| # Prepare list of devices by scanning the model and cell directories. |
| pathtop = '../../libraries/sky130_fd_pr/' + version |
| (filesdict, subcktdict, includedict, modfilesdict) = find_all_devices.find_everything(pathtop) |
| for key in subcktdict: |
| devicelist.append(key) |
| |
| passes = [] |
| fails = [] |
| baseline_entries = [] |
| reasons = {} |
| |
| # Allow ngspice runs to happen in parallel. |
| with multiprocessing.Pool() as pool: |
| results = [] |
| |
| for device in devicelist: |
| results.append(pool.apply_async(do_for_device, (device,))) |
| |
| for r in results: |
| (newpasses, newfails, new_baseline_entries, new_reasons) = r.get(timeout=120) |
| |
| passes.extend(newpasses) |
| fails.extend(newfails) |
| baseline_entries.extend(new_baseline_entries) |
| reasons.update(new_reasons) |
| |
| # Output which devices passed and failed |
| |
| print() |
| print('Simulation summary:') |
| |
| print() |
| print('Passing devices:') |
| for passing in passes: |
| print(passing) |
| |
| print() |
| print('Failing devices:') |
| for failing in fails: |
| print(failing) |
| print(' Reason: ' + reasons[failing]) |
| |
| # Output pass / fail summary |
| |
| print() |
| print('Final results:') |
| print('Passes: ' + str(len(passes))) |
| print('Fails: ' + str(len(fails))) |
| |
| # Repeat the summary in a file "output.log" |
| |
| with open('output.log', 'w') as ofile: |
| print('Simulation summary:', file=ofile) |
| |
| print('', file=ofile) |
| print('Passing devices:', file=ofile) |
| for passing in passes: |
| print(passing, file=ofile) |
| |
| print('', file=ofile) |
| print('Failing devices:', file=ofile) |
| for failing in fails: |
| print(failing, file=ofile) |
| print(' Reason: ' + reasons[failing], file=ofile) |
| |
| # Output pass / fail summary |
| |
| print('', file=ofile) |
| print('Final results:', file=ofile) |
| print('Passes: ' + str(len(passes)), file=ofile) |
| print('Fails: ' + str(len(fails)), file=ofile) |
| |
| # Output baseline values if any were generated |
| |
| if len(baseline_entries) > 0: |
| with open('baseline_results.txt', 'w') as ofile: |
| for entry in baseline_entries: |
| print(entry, file=ofile) |
| |
| if len(fails) > 0: |
| sys.exit(1) |
| else: |
| sys.exit(0) |