Skip to content

Commit

Permalink
Merge pull request #56 from CINF/feature/driver_updates
Browse files Browse the repository at this point in the history
Feature/driver updates
  • Loading branch information
robertjensen committed Mar 10, 2023
2 parents e26f987 + 0fdc110 commit 02d6bdf
Show file tree
Hide file tree
Showing 8 changed files with 731 additions and 86 deletions.
121 changes: 121 additions & 0 deletions PyExpLabSys/drivers/keithley_2000.py
@@ -0,0 +1,121 @@
""" Simple driver for Keithley 2000 DMM """
from PyExpLabSys.drivers.scpi import SCPI


class Keithley2000(SCPI):
"""
Simple driver for Keithley 2000 DMM
"""

def __init__(self, interface, hostname='', device='',
baudrate=9600, gpib_address=None):
if interface == 'serial':
SCPI.__init__(self, interface=interface, device=device,
baudrate=baudrate, line_ending='\n')
self.comm_dev.timeout = 2
self.comm_dev.rtscts = False
self.comm_dev.xonxoff = False
if interface == 'gpib':
SCPI.__init__(self, interface=interface, gpib_address=gpib_address)

def set_bandwith(self, measurement='voltage:ac', bandwidth=None):
scpi_cmd = 'SENSE:{}:DETector:BANDwidth'.format(measurement)
if bandwidth is not None:
DMM.scpi_comm(scpi_cmd + ' {}'.format(bandwidth))
value_raw = DMM.scpi_comm(scpi_cmd + '?')
value = float(value_raw)
return value

def set_range(self, value: float):
"""
Set the measurement range of the device, 0 will indicate auto-range
"""
if value > 1000:
value = 1000
if value < 0:
value = 0
if value == 0:
self.scpi_comm(':SENSE:VOLT:DC:RANGE:AUTO ON')
self.scpi_comm(':SENSE:VOLT:AC:RANGE:AUTO ON')
else:
self.scpi_comm(':SENSE:VOLT:DC:RANGE {:.5f}'.format(value))
self.scpi_comm(':SENSE:VOLT:AC:RANGE {:.5f}'.format(value))

actual_range_raw = self.scpi_comm(':SENSE:VOLTAGE:AC:RANGE?')
actual_range = float(actual_range_raw)
return actual_range

def set_integration_time(self, nplc: float = None):
"""
Set the measurement integration time
"""
if nplc is not None:
if nplc < 0.01:
nplc = 0.01
if nplc > 60:
nplc = 60
self.scpi_comm('SENSE:VOLTAGE:AC:NPLCYCLES {}'.format(nplc))
# self.scpi_comm('SENSE:VOLTAGE:DC:NPLCYCLES {}'.format(nplc))
current_nplc = float(self.scpi_comm('SENSE:VOLTAGE:AC:NPLCYCLES?'))
return current_nplc

def configure_measurement_type(self, measurement_type=None):
""" Setup measurement type """
if measurement_type is not None:
# todo: Ensure type is an allow type!!!!
self.scpi_comm(':CONFIGURE:{}'.format(measurement_type))
actual = self.scpi_comm(':CONFIGURE?')
return actual

def read_ac_voltage(self):
""" Read a voltage """
raw = self.scpi_comm(':MEASURE:VOLTAGE:AC?')
voltage = float(raw)
return voltage

def next_reading(self):
""" Read next reading """
t0 = time.time()
while not self.measurement_available():
time.sleep(0.001)
if (time.time() - t0) > 10:
# Todo: This is not good enough
print('Keithley 2000 TIMEOUT!')
break
raw = self.scpi_comm(':DATA?')
voltage = float(raw)
return voltage

def measurement_available(self):
meas_event = int(self.scpi_comm('STATUS:MEASUREMENT:EVENT?'))
mav_bit = 5
mav = (meas_event & 2**mav_bit) == 2**mav_bit
return mav


if __name__ == '__main__':
import time

GPIB = 16
DMM = Keithley2000(interface='gpib', gpib_address=GPIB)

# TODO! Something changes with the configuration when this
# command is called, measurement is much slower and
# NPLC command fails?!?!
# print(DMM.configure_measurement_type('volt:ac'))

DMM.set_range(0.1)
print(DMM.set_integration_time(2))
# print(DMM.set_bandwith())
# print(DMM.set_integration_time(10))

for i in range(0, 20):
# time.sleep(0.05)
t = time.time()
# meas_event = DMM.scpi_comm('STATUS:MEASUREMENT:EVENT?')
# print(bin(int(meas_event)))
while not DMM.measurement_available():
time.sleep(0.05)
reading = DMM.next_reading()
dt = time.time() - t
print('Time: {:.2f}ms. AC {:.3f}uV'.format(dt * 1e3, reading * 1e6))
86 changes: 86 additions & 0 deletions PyExpLabSys/drivers/keithley_2182.py
@@ -0,0 +1,86 @@
""" Simple driver for Keithley 2182 Nanovolt Meter """
from PyExpLabSys.drivers.scpi import SCPI


class Keithley2182(SCPI):
"""
Simple driver for Keithley 2182 Nanovolt Meter
Actual implementation performed on a 2182a - please
double check if you have a 2182.
"""

def __init__(self, interface, hostname='', device='',
baudrate=9600, gpib_address=None):
if interface == 'serial':
SCPI.__init__(self, interface=interface, device=device,
baudrate=baudrate, line_ending='\n')
self.comm_dev.timeout = 2
self.comm_dev.rtscts = False
self.comm_dev.xonxoff = False
if interface == 'gpib':
SCPI.__init__(self, interface=interface, gpib_address=gpib_address)

# For now, turn off continous trigger - this might need reconsideration
self.scpi_comm('INIT:CONT OFF')

def set_range(self, channel1: float = None, channel2: float = None):
"""
Set the measurement range of the device, 0 will indicate auto-range
"""
if channel1 is not None:
if channel1 > 120:
channel1 = 120
if channel1 == 0:
self.scpi_comm(':SENSE:VOLT:CHANNEL1:RANGE:AUTO ON')
else:
self.scpi_comm(':SENSE:VOLT:CHANNEL1:RANGE {:.2f}'.format(channel1))

if channel2 is not None:
if channel2 > 12:
channel2 = 12
if channel2 == 0:
self.scpi_comm(':SENSE:VOLTAGE:CHANNEL2:RANGE:AUTO ON')
else:
self.scpi_comm(':SENSE:VOLT:CHANNEL2:RANGE {:.2f}'.format(channel2))

actual_channel1_raw = self.scpi_comm(':SENSE:VOLTAGE:CHANNEL1:RANGE?')
actual_channel2_raw = self.scpi_comm(':SENSE:VOLTAGE:CHANNEL2:RANGE?')
range1 = float(actual_channel1_raw)
range2 = float(actual_channel2_raw)
return range1, range2

def set_integration_time(self, nplc: float = None):
"""
Set the measurement integration time
"""
if nplc is not None:
if nplc < 0.01:
nplc = 0.01
if nplc > 60:
nplc = 60
self.scpi_comm('SENSE:VOLTAGE:NPLCYCLES {}'.format(nplc))
current_nplc = float(self.scpi_comm('SENSE:VOLTAGE:NPLCYCLES?'))
return current_nplc

def read_voltage(self, channel: int):
""" Read the measured voltage """
if channel not in (1, 2):
return None
self.scpi_comm(":SENSE:FUNC 'VOLT:DC'")
self.scpi_comm(':SENSE:CHANNEL {}'.format(channel))
raw = self.scpi_comm(':READ?')
voltage = float(raw)
return voltage


if __name__ == '__main__':
GPIB = 7
NVM = Keithley2182(interface='gpib', gpib_address=GPIB)

print(NVM.set_range(1, 0.01))
print(NVM.set_integration_time(10))

for i in range(0, 10):
print()
print('Channel 1: {:.3f}uV'.format(NVM.read_voltage(1) * 1e6))
print('Channel 2: {:.3f}uV'.format(NVM.read_voltage(2) * 1e6))
183 changes: 183 additions & 0 deletions PyExpLabSys/drivers/keithley_2400.py
@@ -0,0 +1,183 @@
""" Simple driver for Keithley 2400 SMU """
from PyExpLabSys.drivers.scpi import SCPI


class Keithley2400(SCPI):
""" Simple driver for Keithley 2400 SMU """

def __init__(self, interface, hostname='', device='',
baudrate=9600, gpib_address=None):
if interface == 'serial':
SCPI.__init__(self, interface=interface, device=device,
baudrate=baudrate, line_ending='\n')
self.comm_dev.timeout = 2
self.comm_dev.rtscts = False
self.comm_dev.xonxoff = False
if interface == 'lan':
SCPI.__init__(self, interface=interface, hostname=hostname)
if interface == 'gpib':
SCPI.__init__(self, interface=interface, gpib_address=gpib_address)

def output_state(self, output_state: bool = None):
""" Turn the output on or off """
if output_state is not None:
if output_state:
self.scpi_comm('OUTPUT:STATE 1')
else:
self.scpi_comm('OUTPUT:STATE 0')
actual_state_raw = self.scpi_comm('OUTPUT:STATE?')
actual_state = actual_state_raw[0] == '1'
return actual_state

def set_current_measure_range(self, current_range=None):
""" Set the current measurement range """
# TODO!
raise NotImplementedError

def set_integration_time(self, nplc: float = None):
"""
Set the measurement integration time
In principle the current ant voltage value can be set
independently, but for now they are synchronized
"""
if nplc is not None:
if nplc < 0.01:
nplc = 0.01
if nplc > 10:
nplc = 10
self.scpi_comm('SENSE:CURRENT:NPLCYCLES {}'.format(nplc))
self.scpi_comm('SENSE:VOLTAGE:NPLCYCLES {}'.format(nplc))
current_nplc = float(self.scpi_comm('SENSE:CURRENT:NPLCYCLES?'))
return current_nplc

def _parse_status(self, status_string):
status_table = {
0: ('OFLO', 'Measurement was made while in over-range'),
1: ('Filter', 'Measurement was made with the filter enabled'),
# 2: ('Front/Rear', 'FRONT terminals are selected'),
3: ('Compliance', 'In real compliance'),
4: ('OVP', 'Over voltage protection limit was reached'),
5: ('Math', 'Math expression (calc1) is enabled'),
6: ('Null', 'Null is enabled'),
7: ('Limits', 'Limit test (calc2) is enabled'),
# Bits 8 and 9 (Limit Results) — Provides limit test results
# (see scpi command reference 18-51)
10: ('Auto-ohms', 'Auto-ohms enabled'),
11: ('V-Meas', 'V-Measure is enabled'),
12: ('I-Meas', 'I-Measure is enabled'),
13: ('Ω-Meas', 'Ω-Measure is enabled'),
14: ('V-Sour', 'V-Source used'),
15: ('I-Sour', 'I-Source used'),
16: ('Range Compliance', 'In range compliance'),
17: ('Offset Compensation', 'Offset Compensated Ohms is enabled'),
18: ('Contact check failure', '(see Appendix F in manual'),
# Bits 19, 20 and 21 (Limit Results) — Provides limit test results
# (see scpi command reference 18-51)
22: ('Remote Sense', '4-wire remote sense selected'),
23: ('Pulse Mode', 'In the Pulse Mode')
}

status_messages = []
status_value = int(float(status_string))
# Strip 0b from the string and fill to 24 bits
bin_status = bin(status_value)[2:].zfill(24)
bin_status = bin_status[::-1] # Reverse string, to get correct byte order
for i in range(0, 24):
bit_value = int(bin_status[i]) == 1
if bit_value and i in status_table:
status_messages.append(status_table[i])
return status_messages

def read_current(self):
"""
Read the measured current
Returns None if the output is off.
"""
if self.output_state():
raw = self.scpi_comm('MEASURE:CURRENT?')
else:
raw = None
if raw is None:
return

# Values are: voltage, current, ohm, time, status
# Only the current is measured, voltage is either
# NaN or the source-setpoint.
values = raw.split(',')
current = float(values[1])
# timestamp = float(values[3])
# print(self._parse_status(values[4]))
# Also return timestamp?
return current

def read_voltage(self):
""" Read the measured voltage """
if self.output_state():
raw = self.scpi_comm('MEASURE:VOLTAGE?')
else:
raw = None
if raw is None:
return

# Values is: voltage, current, ohm, time, status
# Only the voltage is measured, current is either
# NaN or the source-setpoint.
values = raw.split(',')
voltage = float(values[0])
# timestamp = float(values[3])
# print(self._parse_status(values[4]))
# Also return timestamp?
return voltage

def set_source_function(self, function=None):
if function in ('i', 'I'):
self.scpi_comm('SOURCE:FUNCTION CURRENT')
if function in ('v', 'V'):
self.scpi_comm('SOURCE:FUNCTION VOLTAGE')
actual_function = self.scpi_comm('SOURCE:FUNCTION?')
return actual_function

def set_current_limit(self, current: float = None):
""" Set the desired current limit """
if current is not None:
self.scpi_comm('CURRENT:PROTECTION {:.9f}'.format(current))
actual = self.scpi_comm('CURRENT:PROTECTION?')
return actual

def set_voltage_limit(self, voltage: float = None):
""" Set the desired voltate limit """
if voltage is not None:
self.scpi_comm('VOLTAGE:PROTECTION {:.9f}'.format(voltage))
actual = self.scpi_comm('VOLTAGE:PROTECTION?')
return actual

def set_current(self, current: float):
""" Set the desired current """
self.scpi_comm('SOURCE:CURRENT {:.9f}'.format(current))
return True

def set_voltage(self, voltage: float):
""" Set the desired current """
self.scpi_comm('SOURCE:VOLT {:.9f}'.format(voltage))
return True


if __name__ == '__main__':
GPIB = 22
SMU = Keithley2400(interface='gpib', gpib_address=GPIB)
SMU.set_source_function('v')
SMU.output_state(True)
print(SMU.set_current_limit(100e-6))

# SMU.set_voltage_limit(1e-1)
SMU.set_voltage(0.0)
print(SMU.read_software_version())
# SMU.output_state(True)

# print(SMU.output_state())

current = SMU.read_current()
voltage = SMU.read_voltage()

print('Current: {:.1f}uA. Voltage: {:.2f}mV. Resistance: {:.1f}ohm'.format(
current * 1e6, voltage * 1000, voltage / current))

0 comments on commit 02d6bdf

Please sign in to comment.