blob: cf4984d30f18285ba6463c5a1ea8b3c64a95696e [file] [log] [blame]
#!/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)