As proposed by @Axel here , I’m posting the updated Keithley SMU 236 driver which includes the capability of emitting pulses. Instead of creating a commit on github, I would like to discuss some details here first.
- The driver works for single pulses only because SweepMe expects to get back only one pair of values. We could average over multiple pulses but I don’t think this should be implemented in the driver as the default behaviour.
- The sourcing range must be set manually to include all values that are part of the sweep, e.g. if you sweep voltages from 1V to 5V, you need to choose the 11V range.
- The measurement range must be set manually to include the chosen compliance limit, e.g. if you set compliance to 5mA, you need to choose at least the 10mA range.
- The instrument will cleverly recognize if a requested pulse timing could not be achieved e.g. due to slow rise times and short t_on times. This is displayed on the instrument via a message, but no error is retrieved in SweepMe to check for this.
- Any kind of loop, at least in 1.5.7.4, just repeats the measurement, but not the pulsing.
Question regarding 2) and 3): should certain checks be implemented to verify a sane setting? The ranges would have to be rewritten to hand over a numerical value to compare against the sweep value and compliance limit.
Question regarding 4): should we retrieve the error status to check for such a thing?
Question regarding 5): should this behaviour be changed?
Best wishes,
Christian
# This Device Class is published under the terms of the MIT License.
# Required Third Party Libraries, which are included in the Device Class
# package for convenience purposes, may have a different license. You can
# find those in the corresponding folders or contact the maintainer.
#
# MIT License
#
# Copyright (c) 2018 Axel Fischer (sweep-me.net)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# SweepMe! device class
# Type: SMU
# Device: Keithley 236
import numpy as np
import time
from EmptyDeviceClass import EmptyDevice
class Device(EmptyDevice):
def __init__(self):
super().__init__()
self.shortname = "Keithley236"
self.variables =["Voltage", "Current"]
self.units = ["V", "A"]
self.plottype = [True, True] # True to plot data
self.savetype = [True, True] # True to save data
self.port_manager = True
self.port_types = ["GPIB"]
# operating mode
self.sources = {
"Voltage [V]": "0",
"Current [A]": "1",
}
# sourcing range
self.sranges = {
"Auto": "0",
"1 nA | 1.1 V (1.5 V 238 only)": "1",
"10 nA | 11 V (15 V 238 only)": "2",
"100 nA | 110 V": "3",
"1 uA | (1100 V 237 only)": "4",
"10 uA": "5",
"100 uA": "6",
"1 mA": "7",
"10 mA": "8",
"100 mA": "9",
"1A (238 only)": "10",
}
# measuring range
self.mranges = {
"Auto": "0",
"1 nA | 1.1 V (1.5 V 238 only)": "1",
"10 nA | 11 V (15 V 238 only)": "2",
"100 nA | 110 V": "3",
"1 uA | (1100 V 237 only)": "4",
"10 uA": "5",
"100 uA": "6",
"1 mA": "7",
"10 mA": "8",
"100 mA": "9",
"1A (238 only)": "10",
}
def set_GUIparameter(self):
GUIparameter = {
"SweepMode" : list(self.sources.keys()),
"RouteOut": ["Rear"],
"Speed": ["Fast", "Medium", "Slow"],
"Average":1,
"4wire": False,
"Compliance": 100e-6,
"Sourcing Range": list(self.sranges.keys()),
"Measuring Range": list(self.mranges.keys()),
"CheckPulse": False,
"PulseOnTime": 0.100,
"PulseOffTime": 0.100,
"PulseOffLevel": 0.0,
# multiple pulses currently not supported
#"PulseCount": 1,
}
return GUIparameter
def get_GUIparameter(self, parameter = {}):
self.four_wire = parameter['4wire']
self.route_out = parameter['RouteOut']
self.source = parameter['SweepMode']
self.protection = parameter['Compliance']
self.speed = parameter['Speed']
self.average = int(parameter['Average'])
self.srange = parameter['Sourcing Range']
self.mrange = parameter['Measuring Range']
self.checkpulse = parameter['CheckPulse']
# multiple pulses currently not supported
#self.pulsecount = parameter['PulseCount']
self.pulsecount = "1"
self.ton = parameter['PulseOnTime']
self.toff = parameter['PulseOffTime']
self.bias = parameter['PulseOffLevel']
if self.average < 1:
self.average = 1
if self.average > 100:
self.average = 100
def initialize(self):
if not int(round(np.log2(self.average))) in [0,1,2,3,4,5]:
new_readings = int(round(np.log2(self.average)))
msg = ("Please use 1, 2, 4, 8, 16, or 32 for the number of averages. Changed it to %s." % new_readings)
raise Exception(msg)
def configure(self):
if self.sranges[self.srange] == "0" and self.checkpulse == True:
msg = ("AUTO sourcing range not possible in pulse mode, please choose manual range.")
raise Exception(msg)
if self.mranges[self.mrange] == "0" and self.checkpulse == True:
msg = ("AUTO measuring range not possible in pulse mode, please choose manual range.")
raise Exception(msg)
if self.checkpulse == True:
# Sourcemode for pulses
self.port.write("F%s,1X" % self.sources[self.source])
# Output data format for pulse mode
self.port.write("G5,2,1X")
# Configure trigger for pulses
self.port.write("T4,8,0,0X")
else:
# Sourcemode for DC
self.port.write("F%s,0X" % self.sources[self.source])
# Output data format for DC mode
self.port.write("G5,2,0X")
# Configure trigger for DC mode
self.port.write("T4,0,0,0X")
# Protection
self.port.write("L%s,%sX" % (self.protection, self.mranges[self.mrange]))
if self.speed == "Fast":
self.nplc = 0
if self.speed == "Medium":
self.nplc = 1
if self.speed == "Slow":
self.nplc = 3
self.port.write("S%sX" % self.nplc) #0=0.4ms,1=4ms,2=17ms,3=20ms;
# 4-wire sense
if self.four_wire:
self.port.write("O1X")
else:
self.port.write("O0X")
# Averaging
if self.average < 32:
readings = int(round(np.log2(self.average)))
else:
readings = 5
self.port.write("P%sX" % readings)
# Number Readings
# 0 1 (disabled)
# 1 2
# 2 4
# 3 8
# 4 16
# 5 32
# enable triggers in case they were disabled
self.port.write("R1X")
def deinitialize(self):
self.port.write("O0X")
def poweron(self):
self.port.write("N1X")
def poweroff(self):
self.port.write("N0X")
def apply(self):
if self.checkpulse == False:
# in case of Sweep Type "DC", the "B" command applies "self.value" as the DC level output and sets the measuring range
self.port.write("B%s,%s,0X" % (self.value, self.sranges[self.srange]))
else:
# in case of Sweep Type "Pulse", the "B" command applies "self.bias" as the bias level for the pulses instead, together with the measuring range
# note: the bias command and the pulse command must use the same sourcing range as otherwise a range switch might occur in between, leading to the instrument not being able to meet the requested pulse times
self.port.write("B%s,%s,0X" % (self.bias, self.sranges[self.srange]))
# pulses command "Q3,(level),(range),(pulses),(toN),(toFF)"
# the gui requests all times to be entered in seconds but the instrument expects the time values in full figure milliseconds
self.port.write("Q3,%s,%s,%s,%s,%sX" % (self.value, self.sranges[self.srange], self.pulsecount, int(float(self.ton)*1000), int(float(self.toff)*1000)))
def measure(self):
self.port.write("H0X")
def call(self):
if self.source.startswith("Voltage"):
v,i = self.port.read().split(",")
if self.source.startswith("Current"):
i,v = self.port.read().split(",")
return [float(v), float(i)]
def finish(self):
pass