Keithley 236 SMU driver update: added pulse capability

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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
1 Like

Hi Christian,

first of all thank you for working on the pulse mode for the 23x series. Actually, I did not know that they even can do pulses and happy to learn something new here. I could imagine that also some of our other users might enjoy this new feature.

Regarding your questions:

Sanity checks: If you contribute a driver you are happy with and that improves the current driver, we are very glad to receive it. So there is not “must” when contributing drivers. Of course, some checks can help wrong handling and can also help other users in your lab to make correct measurements. Here, it is possible to either check for each combination of parameters or use some kind of brute-force method by applying a new configuration or value and check whether there is an error as you describe in your second question. This often has the advantage that all errors are catched, and the maintenance effort is lower. Here, a simple and straight-forward to understand driver can be often better than the “perfect” driver that however is more difficult to comprehend for other users and excludes them to add their improvements. So, at the end it remains also a question of personal favor. If a problem however is obvious like the measurement range must include in the selected compliance/protection, it would make sense to add this as a separate check.

Because there is also an “Auto” ranging mode for sourcing and measuring which is the default, new or unexperienced users will still have an easy start and more experienced users can use fixed measure ranges.

Checking pulse errors: If there is a error status to check, it makes sense to read it out and compare as otherwise unsupervised long-term measurements might result in complete non-sense. Still, it is up to you as a contributor to decide on your own whether you like to spend the extra time. If you do not add it, you could add a description to the driver by adding static variable “description” to the Device class where one can use richtext to inform users that pulses are not checked. We are working on a release that will allow every module and every driver to have an own description so that also SMU drivers will soon be able to visualize their description.

Repetition of pulses: Because you add the pulse to apply function, a repetition in your active branch does not result in a new pulse because apply is only called when the sweep value (i.e. the set value) is changed. As you probably have always the same pulse on level voltage, there is actually no change of the sweep value that is used in apply as “self.value”. So the behavior is correct. However, the question is now how to understand our semantic functions. “apply” sound indeed like a new pulse should be applied, but actually no new configuration or value is applied. Thus, sending the pulse can be also interpreted as “start the measurement” and therefore the solution is to move this part in case of pulsing the “measure” function. This puts another question on whether the command “H0X” is correctly used. Does it really start the measurement or just the retrieve of the value from the buffer? In a DC measurement, I guess it starts the measurement and also triggers the instrument. In case of a pulse measurement, “H0X” might rather retrieve the values from the buffer that previously have been stored.

I think “H0X” can remain in “measure”, but I would shift sending the pulse to “measure” as well.

If you however, want to pulse with multiple instruments at the same time, I would also split triggering the pulse and retrieving the result, so that first all instruments are triggered to perform their pulsed measurements and in another phase like “request_result” or “read_result”, the data could be transmitted.

Hope this helps to get the basic idea and please check on your own again what the actual meaning of the commands are and how to use them.

Let me know if you have any questions to this.

Thanks and best
Axel

1 Like

As far as I can see you are are using the pulse count to apply multiple pulses. Often, they are automatically averaged by the instrument and returned. It is basically possible to also return a list of voltages and a list of current which would lead to the creation to a single Data 1D file for each measurement point in the sequencer. However, one could also use a Loop module to repeat the pulse (after the driver has been adapted regarding “apply” and “measure”). Unclear is maybe what the driver is doing when the user set average and the pulse count. When average above 1 is not supported in pulse mode, one could throw an exception as well.

1 Like

Hello Axel.

Thanks for the extensive feedback, very helpful. Yes, not only is the 236/7/8 capable of doing so, the pulses look very clean and presice as well.

The H0X command is the immediate trigger of either a DC or pulse sequence that contains one (or several, but not implemented here) measurment(s). The measurement cannot be seperated from the appliance of the voltage, DC or pulses. For a delayed start of a measurement, the command set of the instrument offers a delay parameter. But afaik, you cannot seperate the measurement from the appliance of voltage or current. I think we should leave it in “apply”, also for backward compatibility. Right now, DC voltages/currents are also measured at the time when they are applied. The repetitive appliance of a pulse of the same value could be done by inserting something like a 0V pulse afterwards on the same instrument and loop both step, hmm?

I’ll insert a check for the compliance and timing settings then. For the compliance check, I can doublicate the “range” dictionary but change it so that each entry directs to the numerical value instead of the SCPI command option. I can then use the selected entry from the GUI to pick out the numerical value of the corresponding range and compare that to the “self.value”. For the timing issue, I think there is a status register to be read out that can be done prior to applying “H0X”.

I’ll get back to you as soon as I have implemented the changes.

Cheers,
Christian

1 Like

Hi Christian,

as far as I understood, repetitive DC measurements work fine, even if the voltage is not changed, but pulses are not applied again.

Yes, I would keep the code for DC measurements as is, but in case of a pulsed measurement, it might be that the instrument need to be made ready for another pulse by the commands that you use in ‘apply’ so far. If this is the case, you could only move the pulse part of the apply function to measure or even better to trigger_ready which is a semantic function that can be used to make an instrument ready to receive another trigger.

I can imagine that this could solve your problems.

Best, Axel

1 Like

Hello Axel.

I’ve implemented all functionality as discussed. For the time being, I would like to keep the pulsing alongside the DC output for consistency.

Summary of changes with respect to the original driver:

  • GPIB-EOL modification to simplify the command strings and prevent any „Value Out of Range“ error during GPIB initialization (as tested in SweepMe v1.5.7.5)
  • 236/237/238 model identicifation and check for parameters
  • pulse mode now possible
  • manual selection of measurement range (requirement for pulse mode)
  • manual selection of sourcing range (requirement for pulse mode)
  • optional check of combination of selected measurement range and compliance limit
  • optional check of combination of selected sourcing range and sweep value
  • extended commenting incl. old code

I’ve accidentially added the Keithley 236 to my main branch which leads to the problem that I cannot open a pull request without requesting a second pull for the previously pulled Keithley 199. @Franz : can you update the code in the Keithley 236 with my proposed changes?

Best wishes,
Christian

Hello Christian,
thanks for your contribution!
I have fixed the commit history by rebasing your Keithley_236 commit on the origin-main branch.
I have created a PR: Extend SMU Keithley 236 Driver by franz-sweepMe · Pull Request #177 · SweepMe/instrument-drivers · GitHub

There are two comments by the Gemini Review Assistent, can you please answer them? The other comments I have already resolved.

Hello Felix.

I synced my fork to your base, resulting in removing the old K236 commit that was flawed as I found out during a recent longer measurement session.

There is a new commit with an updated version explaining the changes. I’ll go though the Gemini proposals for that one once they are available.

Best wishes,
Christian

1 Like

Hello Christian,

Thanks for your continued work on the driver!
I have reviewed your recent pull request and added a few short comments.
The Gemini Comments are more or less the same as in the original PR. You can also make the changes in your new PR and I can resolve them in the original PR.

1 Like

Hi Franz.

Still getting used to Github. I overwrote your changes by accident as the driver on the lab pc running sweepme with the K236 attached to it was still using the original version.

I now wrote your old code snippets back into the driver and used Gemini’s suggestions as they were good feedback. If I did it right, my pull request should now contain the updated version (hopefully).

Best wishes,
Christian

PS: the GPIB delay for old, brown Keithley’s seems to be the rule rather than the expection. It slows down the measurement speed but it proves to be more bulletproof so I think it is worth the reduction of speed.

The new driver version is now available in SweepMe!.

1 Like