""" Driver class for Keithley 2606B.
The following programming example illustrates the setup and command
sequence of a basic source-measure procedure with the following parameters:
• Source function and range: voltage, autorange
• Source output level: 5 V
• Current compliance limit: 10 mA
• Measure function and range: current, 10 mA
-- Restore 2606B defaults.
smua.reset()
-- Select voltage source function.
smua.source.func = smua.OUTPUT_DCVOLTS
-- Set source range to auto.
smua.source.autorangev = smua.AUTORANGE_ON
-- Set voltage source to 5 V.
smua.source.levelv = 5
-- Set current limit to 10 mA.
smua.source.limiti = 10e-3
-- Set current range to 10 mA.
smua.measure.rangei = 10e-3
-- Turn on output.
smua.source.output = smua.OUTPUT_ON
-- Print and place the current reading in the reading buffer.
print(smua.measure.i(smua.nvbuffer1))
-- Turn off output.
smua.source.output = smua.OUTPUT_OFF
"""
from . import VISAInstrumentDriver
from lightlab.equipment.visa_bases.driver_base import TCPSocketConnection
from lightlab.laboratory.instruments import Keithley
import socket
import numpy as np
import time
from lightlab import logger
[docs]class Keithley_2606B_SMU(VISAInstrumentDriver):
""" Keithley 2606B 4x SMU instrument driver
`Manual: <https://download.tek.com/manual/2606B-901-01B_May_2018_Ref_Man.pdf>`__
Usage: Unavailable
Capable of sourcing current and measuring voltage, as a Source Measurement Unit.
"""
instrument_category = Keithley
tsp_node = None
channel = None
MAGIC_TIMEOUT = 10
_latestCurrentVal = 0
_latestVoltageVal = 0
currStep = 0.1e-3
voltStep = 0.3
rampStepTime = 0.05 # in seconds.
def __init__(
self,
name=None,
address=None,
tsp_node: int = None,
channel: str = None,
**visa_kwargs
):
"""
Args:
tsp_node: Number from 1 to 64 corresponding to the
pre-configured TSP node number assigned to each module.
channel: 'A' or 'B'
"""
if channel is None:
logger.warning("Forgot to select a channel: either 'A', or 'B'")
elif channel not in ("A", "B", "a", "b"):
raise RuntimeError("Select a channel: either 'A', or 'B'")
else:
self.channel = channel.upper()
if tsp_node is None:
logger.warning("Forgot to specify a tsp_node integer number between 1 and 64.")
elif not isinstance(tsp_node, int):
raise RuntimeError(
"Please specify a tsp_node integer number between 1 and 64."
)
elif not 1 <= tsp_node <= 64:
raise RuntimeError("Invalid tsp_node. Valid numbers between 1 and 64.")
self.tsp_node = tsp_node
visa_kwargs["tempSess"] = visa_kwargs.pop("tempSess", True)
VISAInstrumentDriver.__init__(self, name=name, address=address, **visa_kwargs)
self.reinstantiate_session(address, visa_kwargs["tempSess"])
# BEGIN TCPSOCKET METHODS
[docs] def reinstantiate_session(self, address, tempSess):
if address is not None:
# should be something like ['TCPIP0', 'xxx.xxx.xxx.xxx', '6501', 'SOCKET']
address_array = address.split("::")
self._tcpsocket = TCPSocketConnection(
ip_address=address_array[1],
port=int(address_array[2]),
timeout=self.MAGIC_TIMEOUT,
)
[docs] def open(self):
if self.address is None:
raise RuntimeError("Attempting to open connection to unknown address.")
try:
self._tcpsocket.connect()
super().open()
except socket.error:
self._tcpsocket.disconnect()
raise
[docs] def close(self):
self._tcpsocket.disconnect()
def _query(self, queryStr):
with self._tcpsocket.connected() as s:
s.send(queryStr)
i = 0
old_timeout = s.timeout
s.timeout = self.MAGIC_TIMEOUT
received_msg = ""
while i < 1024: # avoid infinite loop
recv_str = s.recv(1024)
received_msg += recv_str
if recv_str.endswith("\n"):
break
s.timeout = 1
i += 1
s.timeout = old_timeout
return received_msg.rstrip()
[docs] def query(self, queryStr, expected_talker=None):
ret = self._query(queryStr)
if expected_talker is not None:
if ret != expected_talker:
log_function = logger.warning
else:
log_function = logger.debug
log_function(
"'%s' returned '%s', expected '%s'", queryStr, ret, str(expected_talker)
)
else:
logger.debug("'%s' returned '%s'", queryStr, ret)
return ret
[docs] def write(self, writeStr):
with self._tcpsocket.connected() as s:
logger.debug("Sending '%s'", writeStr)
s.send(writeStr)
time.sleep(0.05)
# END TCPSOCKET METHODS
@property
def smu_string(self):
if self.channel.upper() == "A":
return "smua"
elif self.channel.upper() == "B":
return "smub"
else:
raise RuntimeError(
"Unexpected channel: {}, should be 'A' or 'B'".format(self.channel)
)
@property
def smu_full_string(self):
return "node[{N}].{smuX}".format(N=self.tsp_node, smuX=self.smu_string)
[docs] def query_print(self, query_string, expected_talker=None):
time.sleep(0.01)
query_string = "print(" + query_string + ")"
return self.query(query_string, expected_talker=expected_talker)
[docs] def smu_reset(self):
self.write(
"node[{tsp_node}].{smu_ch}.reset()".format(
tsp_node=self.tsp_node, smu_ch=self.smu_string
)
)
[docs] def instrID(self):
query_str = (
"print([[Keithley Instruments Inc., Model ]].."
"node[{tsp_node}].model..[[, ]]..node[{tsp_node}].serialno..[[, ]]..node[{tsp_node}].revision)".format(
tsp_node=self.tsp_node
)
)
return self.query(query_str)
[docs] def is_master(self):
""" Returns true if this TSP node is the localnode.
The localnode is the one being interfaced with the Ethernet cable,
whereas the other nodes are connected to it via the TSP-Link ports.
"""
return self.query_print("localnode.serialno") == self.query_print(
"node[{tsp_node}].serialno".format(tsp_node=self.tsp_node)
)
[docs] def tsp_startup(self, restart=False):
""" Ensures that the TSP network is available.
- Checks if tsplink.state is online.
- If offline, send a reset().
"""
state = self.query_print("tsplink.state")
if state == "online" and not restart:
return True
elif state == "offline":
nodes = int(float(self.query_print("tsplink.reset()")))
logger.debug("%s TSP nodes found.", nodes)
return True
[docs] def smu_defaults(self):
self.write("{smuX}.source.offfunc = 0".format(smuX=self.smu_full_string)) # 0 or smuX.OUTPUT_DCAMPS: Source 0 A
self.write("{smuX}.source.offmode = 0".format(smuX=self.smu_full_string)) # 0 or smuX.OUTPUT_NORMAL: Configures the source function according to smuX.source.offfunc attribute
self.write("{smuX}.source.highc = 1".format(smuX=self.smu_full_string)) # 1 or smuX.ENABLE: Enables high-capacitance mode
# self.write("{smuX}.sense = 0".format(smuX=self.smu_full_string)) # 0 or smuX.SENSE_LOCAL: Selects local sense (2-wire)
self.set_sense_mode(sense_mode="local")
[docs] def startup(self):
self.tsp_startup()
self.smu_reset()
self.smu_defaults()
self.write("waitcomplete()")
time.sleep(0.01)
self.query_print('"startup complete."', expected_talker="startup complete.")
[docs] def set_sense_mode(self, sense_mode="local"):
''' Set sense mode. Defaults to local sensing. '''
if sense_mode == "remote":
sense_mode = 1 # 1 or smuX.SENSE_REMOTE: Selects remote sense (4-wire)
elif sense_mode == "local":
sense_mode = 0 # 0 or smuX.SENSE_LOCAL: Selects local sense (2-wire)
else:
sense_mode = 0 # 0 or smuX.SENSE_LOCAL: Selects local sense (2-wire)
self.write("{smuX}.sense = {sense_mode}".format(smuX=self.smu_full_string, sense_mode=sense_mode))
# SourceMeter Essential methods
def _configCurrent(self, currAmps):
currAmps = float(currAmps)
if currAmps >= 0:
currAmps = np.clip(currAmps, a_min=1e-9, a_max=1.0)
else:
currAmps = np.clip(currAmps, a_min=-1, a_max=-1e-9)
self.write(
"{smuX}.source.leveli = {c}".format(smuX=self.smu_full_string, c=currAmps)
)
self._latestCurrentVal = currAmps
def _configVoltage(self, voltVolts):
voltVolts = float(voltVolts)
self.write(
"{smuX}.source.levelv = {v}".format(smuX=self.smu_full_string, v=voltVolts)
)
self._latestVoltageVal = voltVolts
[docs] def setCurrent(self, currAmps):
""" This leaves the output on indefinitely """
currTemp = self._latestCurrentVal
if not self.enable() or self.currStep is None:
self._configCurrent(currAmps)
else:
nSteps = int(np.floor(abs(currTemp - currAmps) / self.currStep))
for curr in np.linspace(currTemp, currAmps, 2 + nSteps)[1:]:
self._configCurrent(curr)
time.sleep(self.rampStepTime)
[docs] def setVoltage(self, voltVolts):
voltTemp = self._latestVoltageVal
if not self.enable() or self.voltStep is None:
self._configVoltage(voltVolts)
else:
nSteps = int(np.floor(abs(voltTemp - voltVolts) / self.voltStep))
for volt in np.linspace(voltTemp, voltVolts, 2 + nSteps)[1:]:
self._configVoltage(volt)
time.sleep(self.rampStepTime)
[docs] def getCurrent(self):
curr = self.query_print("{smuX}.source.leveli".format(smuX=self.smu_full_string))
return float(curr)
[docs] def getVoltage(self):
volt = self.query_print("{smuX}.source.levelv".format(smuX=self.smu_full_string))
return float(volt)
[docs] def setProtectionVoltage(self, protectionVoltage):
protectionVoltage = float(protectionVoltage)
self.write(
"{smuX}.source.limitv = {v}".format(smuX=self.smu_full_string, v=protectionVoltage)
)
[docs] def setProtectionCurrent(self, protectionCurrent):
protectionCurrent = float(protectionCurrent)
self.write(
"{smuX}.source.limiti = {c}".format(smuX=self.smu_full_string, c=protectionCurrent)
)
@property
def compliance(self):
return (self.query_print("{smuX}.source.compliance".format(smuX=self.smu_full_string)) == "true")
[docs] def measVoltage(self):
retStr = self.query_print("{smuX}.measure.v()".format(smuX=self.smu_full_string))
v = float(retStr)
if self.compliance:
logger.warning('Keithley compliance voltage of %s reached', self.protectionVoltage)
logger.warning('You are sourcing %smW into the load.', v * self._latestCurrentVal * 1e-3)
return v
[docs] def measCurrent(self):
retStr = self.query_print("{smuX}.measure.i()".format(smuX=self.smu_full_string))
i = float(retStr) # second number is current always
if self.compliance:
logger.warning('Keithley compliance current of %s reached', self.protectionCurrent)
logger.warning('You are sourcing %smW into the load.', i * self._latestVoltageVal * 1e-3)
return i
@property
def protectionVoltage(self):
volt = self.query_print("{smuX}.source.limitv".format(smuX=self.smu_full_string))
return float(volt)
@property
def protectionCurrent(self):
curr = self.query_print("{smuX}.source.limiti".format(smuX=self.smu_full_string))
return float(curr)
[docs] def enable(self, newState=None):
''' get/set enable state
'''
if newState is not None:
while True:
self.write("{smuX}.source.output = {on_off}".format(smuX=self.smu_full_string, on_off=1 if newState else 0))
time.sleep(0.1)
self.query_print("\"output configured\"", expected_talker="output configured")
retVal = self.query_print("{smuX}.source.output".format(smuX=self.smu_full_string))
is_on = float(retVal) == 1
if bool(newState) == is_on:
break
else:
retVal = self.query_print("{smuX}.source.output".format(smuX=self.smu_full_string))
is_on = float(retVal) == 1
return is_on
def __setSourceMode(self, isCurrentSource):
if isCurrentSource:
source_mode_code = 0
source_mode_letter = 'i'
measure_mode_letter = 'v'
else:
source_mode_code = 1
source_mode_letter = 'v'
measure_mode_letter = 'i'
self.write("{smuX}.source.func = {code}".format(smuX=self.smu_full_string, code=source_mode_code))
self.write("{smuX}.source.autorange{Y} = 1".format(smuX=self.smu_full_string, Y=source_mode_letter))
self.write("{smuX}.measure.autorange{Y} = 1".format(smuX=self.smu_full_string, Y=measure_mode_letter))
[docs] def setVoltageMode(self, protectionCurrent=0.05):
self.enable(False)
self.__setSourceMode(isCurrentSource=False)
self.setProtectionCurrent(protectionCurrent)
self._configVoltage(0)
[docs] def setCurrentMode(self, protectionVoltage=1):
self.enable(False)
self.__setSourceMode(isCurrentSource=True)
self.setProtectionVoltage(protectionVoltage)
self._configCurrent(0)