| #!/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 |
| |
| from plot_mosfet_iv import make_mosfet_iv_plot |
| from plot_mosfet_vth import make_mosfet_vth_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 |
| |
| #------------------------------------------------------------------------------ |
| # Run ngspice on the indicated input file in the "results" directory. |
| #------------------------------------------------------------------------------ |
| |
| def runspice(scriptname, resultname, 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: |
| |
| found = True if resultname == 'none' else False |
| valueline = False |
| returncode = 0 |
| |
| for line in sproc.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) |
| returncode = -1 |
| valueline = False |
| |
| elif resultname != 'none' and resultname in line: |
| found = True |
| valueline = True |
| |
| if sproc.stderr: |
| errors = False |
| messages = [] |
| for line in sproc.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:') |
| for message in messages: |
| print(message) |
| returncode = -1 |
| else: |
| print('Ngspice warnings:') |
| for message in messages: |
| print(message) |
| sproc.stdout.close() |
| return_code = sproc.wait() |
| if return_code != 0: |
| print('Execution error: Ngspice exited with error code' + str(return_code)) |
| return -2 |
| |
| if found: |
| return returncode |
| else: |
| print('Result error: Expected result name ' + resultname + ' but did not receive it.') |
| return -1 |
| |
| #------------------------------------------------------------------------------ |
| # 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() |
| |
| outlines = [] |
| for line in spicelines: |
| newline = line.replace('DEVICENAME', devicename) |
| newline = newline.replace('PDKVERSION', version) |
| newline = newline.replace('CORNER', corner) |
| 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 = 0 |
| fails = 0 |
| baseline_run = False |
| baseline_entries = [] |
| |
| 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): |
| # Inductors not handled (yet)! |
| pass |
| elif cap1rex.match(devicename): |
| devicetype = 'mimcap' |
| elif cap2rex.match(devicename): |
| 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' |
| |
| try: |
| print('Diagnostic: Determined device ' + devicename + ' to be type ' + devicetype) |
| except: |
| print('Unknown device type for device name ' + devicename + '. Cannot simulate.') |
| fails += 1 |
| return (passes, fails, baseline_entries) |
| |
| if baseline_run: |
| expectedval = 'none' |
| |
| if devicetype == 'nfet' or devicetype == 'pfet': |
| genspice(devicetype + '_iv.spice', devicename, version, corner) |
| result = runspice(devicename + '.spice', 'none', expectedval) |
| if result != 0: |
| fails += 1 |
| else: |
| passes += 1 |
| if result != -2: |
| make_mosfet_iv_plot(devicename, version, corner) |
| |
| genspice(devicetype + '_vth.spice', devicename, version, corner) |
| result = runspice(devicename + '.spice', 'threshold voltage', expectedval) |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes += 1 |
| elif result != 0: |
| fails += 1 |
| else: |
| passes += 1 |
| |
| elif devicetype == 'npn' or devicetype == 'pnp': |
| genspice(devicetype + '_iv.spice', devicename, version, corner) |
| result = runspice(devicename + '.spice', 'none', expectedval) |
| if result != 0: |
| fails += 1 |
| else: |
| passes += 1 |
| if result != -2: |
| make_bipolar_iv_plot(devicename, version, corner) |
| |
| genspice(devicetype + '_beta.spice', devicename, version, corner) |
| result = runspice(devicename + '.spice', 'maximum beta', expectedval) |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes += 1 |
| elif result != 0: |
| fails += 1 |
| else: |
| passes += 1 |
| if result != -2: |
| make_bipolar_beta_plot(devicename, version, corner) |
| |
| elif devicetype == 'mimcap': |
| genspice('mimcap_test.spice', devicename, version, corner) |
| result = runspice(devicename + '.spice', 'capacitance', expectedval) |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes += 1 |
| elif result != 0: |
| fails += 1 |
| else: |
| passes += 1 |
| |
| elif devicetype == 'capacitor': |
| genspice('capval_test.spice', devicename, version, corner) |
| result = runspice(devicename + '.spice', 'capacitance', expectedval) |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes += 1 |
| elif result != 0: |
| fails += 1 |
| else: |
| passes += 1 |
| |
| elif devicetype == 'resistor': |
| genspice('resval_test.spice', devicename, version, corner) |
| result = runspice(devicename + '.spice', 'resistance', expectedval) |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes += 1 |
| elif result != 0: |
| fails += 1 |
| else: |
| passes += 1 |
| |
| elif devicetype == 'diode' or devicetype == 'diode_dev': |
| genspice(devicetype + '_vth.spice', devicename, version, corner) |
| result = runspice(devicename + '.spice', 'threshold voltage', expectedval) |
| if baseline_run and isinstance(result, str): |
| baseline_entries.append(devicename + ' ' + result) |
| passes += 1 |
| elif result != 0: |
| fails += 1 |
| else: |
| passes += 1 |
| if result != -2: |
| make_diode_iv_plot(devicename, version, corner) |
| |
| return (passes, fails, baseline_entries) |
| |
| #------------------------------------------------------------------------------ |
| # Main script starts here (no arguments, at least for now) |
| #------------------------------------------------------------------------------ |
| |
| if __name__ == "__main__": |
| |
| if not os.path.exists('results'): |
| os.makedirs('results') |
| |
| with open('devices.txt') as ifile: |
| ilines = ifile.read().splitlines() |
| |
| # To do: Loop through version and corner. For now, fixed. |
| version = 'v0.20.1' |
| corner = 'tt' |
| |
| passes = 0 |
| fails = 0 |
| baseline_entries = [] |
| |
| # Allow ngspice runs to happen in parallel. |
| pool = multiprocessing.Pool() |
| results = [] |
| |
| for line in ilines: |
| if line.strip().startswith('#'): |
| continue |
| |
| results.append(pool.apply_async(do_for_device, (line,))) |
| |
| for r in results: |
| (newpasses, newfails, new_baseline_entries) = r.get() |
| |
| passes += newpasses |
| fails += newfails |
| baseline_entries.extend(new_baseline_entries) |
| |
| print() |
| print('Final results:') |
| print('Passes: ' + str(passes)) |
| print('Fails: ' + str(fails)) |
| |
| # 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 fails > 0: |
| sys.exit(1) |
| else: |
| sys.exit(0) |