Keysight B2902B SMU driver feedback

Dear all.

I worked on getting the Keysight B29xx beta driver functional on our B2902B before joining the forum, but I would like to continue here so that others can participate.

I was using the driver today when I suddenly got a communication error. I noticed that this happened because I had set the compliance limits a bit to low and the voltage had run into it for several measurements. To my surprise, this was audible as well as the SMU was switching channels rapidly.

After some investigation, the reason turned out to be the “Output Protection” function that is hardcoded into the SweepMe driver and causes an immediate switch off of the channel in use down to 0V once the compliance limit is reached, with the sideeffect of returning no measurement result as well (which in turn causes the comm. error)

With standard Keysight programs and other instruments, we are used to the behaviour in which the forced output is just clipped at the compliance value and measurements are proceeded. This behaviour can be accomplished by commenting out the output protection feature:

def initialize(self):
[...somecode...]
self.port.write(":OUTP%s:PROT ON" % self.channel)  # enables over voltage / over current protection

Should we make this behaviour standard, e.g. setting it to “OFF” instead as it is standard in the device as well (or comment it out, as the *RST at the beginning of the initalization would set it to OFF anyway)? Or would it be justificated to make it an option in the GUI?

Best wishes,
Christian

1 Like

Hi Christian,

Yes, I think it should be removed so that the driver works as all other SMU driver that are automatically reducing the voltage when reaching the compliance but not switching off.

I will provide a new version in the next days.

Users that like to stop a measurement when a certain current level is reached can also solve this in SweepMe! using the add-on module “Condition” to trigger the stop of the program or to continue with another task.

Thanks for reporting the issue!
Axel

1 Like

A new version of the driver is uploaded (already publicly available) The output protection is now no more enabled during the “initialize” phase of the driver.

Code can be found here:

Thanks for reporting the issue!

Hi Axel.

I recently noticed that the B2902B can also suffer from the skipping of steps when the floatingpoint value being injected into the SCPI command is larger than allowed, due to rounding errors causing too much trailing zeros. I would recommend to implement the conversion to scientific notation just like in the other SMU drivers we recently talked about.

In addition, I’ve worked on getting the pulse capabilities of the B290xB SMUs functional. I based my programming on the existing Keithley drivers, but Keysight is parameterizing the signal properties differently. The culprit is twofold:

The B290xB SMU defines the point of the start of the measurement in relation to the start of the pulse release. But the rise time of the pulse voltage varies strongly depening on the target amplitude voltage. For high voltage pulses, it can reach up to nearly 1ms. If one chooses a starting time shorter than the rise time, the measurement will partially be based on the ramp-up of the voltage, leading to a lower than expected value.

Second, one must also choose a very short integration time, otherwise the measurement will extend into the ramp-down and even the bias voltage after the pulse, again, leading to a lower than expected value. The fastest integration time available is 0.01 NPLC, which equals 200µs in countries with 50Hz power nets. In essence, this forbids to play safe and increase the time before the start of the measurement as the minimum possible pulse width will be raised for the measurement to complete before the end of the pulse. It is not an issue for pulses in the range of milliseconds, but for microseconds, it is.

Should we just leave the measurement start offset hardcoded to a compromise like 500µs and set the minimum pulse time to 1ms to be on the safe side? Or do we make these parameters editable in the GUI for the user to check the applicability via a scope?

I will upload the new sourcecode after I’ve adapted it to the latest version released by you so there is no work to be done twice.

Best wishes
Christian

1 Like

Hi Christian,

I need to think about your points in detail next week, but maybe I can give you some first answers:

Regarding the floating point value, I will change the handling to the one of the other drivers and upload a new version next week.

Regarding the pulsing, a typical solution could be to first allow everything in the user interface but throwing errors when a configuration does not safely work. So instead of restricting the parameter selection by using a hard-coded time or other preset restrictions, one can add to the “initialize” or “configure” phase some parameter tests that raise an exception when failing. The error message could then inform the user how to change parameters to perform a measurement. It is a bit like a try-except approach for the user interface.
Maybe this is also an option that you could consider.

In general, making such sophisticated measurements like pulsed IV very user-friendly can also backfire sometimes as some users might not question the results anymore and use incorrect data then. Restricting the combination of certain parameters can definitely help to perform such measurements more reliable.

Being a bit more restrictive could then be the better option to guarantee a well working measurement for most users. Advanced users can always create a copy of the driver and customize it or create a script for the add-on module “CustomFunction” to run a very specific procedure.

Thanks and best,
Axel

Hi Axel.

I agree, restricting the driver seems to be the best approch. I’ve inserted conservative figures for the two varibles in question and implemented explanatorily error messages for any attempts to lower them further. Mind you I had to hardcode the time for the measurement start time for now as the existing measurement start time field in the GUI expects a value given in percentage and will round any times given in microseconds to one full second.

Also, the new “Very Fast” NPLC setting 0.01 for the pulse capabilities was added and the values added to the entry descriptions to make the process of combining suitable values for measurement start time, pulse duration, peak voltage and NPLC (essentially measurement time requirement) more transparent.

The code contains the scientific notation for the vlaue transfer into the SCPI command as recently implemented in other SMU drivers. If you are going to tackle the problem in the backend soon, it does not need to get transfered.

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) 2024 SweepMe! GmbH (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: Agilent 29xx


from __future__ import annotations

from pysweepme.EmptyDeviceClass import EmptyDevice


class Device(EmptyDevice):

    def __init__(self):

        super().__init__()

        self.shortname = "Agilent B29xx"

        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 = ["USB", "GPIB"]

        self.port_properties = {
            # "timeout": 10,
        }

        self.commands = {
            "Voltage [V]": "VOLT",  # deprecated, remains for compatibility
            "Current [A]": "CURR",  # deprecated, remains for compatibility
            "Voltage in V": "VOLT",
            "Current in A": "CURR",
        }
        
        self.pulse_mode = True # enables pulsed signal option in GUI

    def set_GUIparameter(self):

        GUIparameter = {
            "SweepMode": ["Voltage in V", "Current in A"],
            "Channel": ["CH1", "CH2"],
            "4wire": False,
            # "RouteOut": ["Front", "Rear"],
            "Speed": ["Very Fast: 0.01NPLC", "Fast: 0.1NPLC", "Medium: 1NPLC", "Slow: 10NPLC"], # NPLCs included to transparently allow for user based measurement time estimations
            "Compliance": 100e-6,
            "CheckPulse": False,
            #"PulseMeasStart_in_s": 750e-6, #for use on the B2902B, defined as the delay for measurement start after pulse release;variable not yet implemented
            "PulseOnTime": 1e-3, #for use on the B2902B, defined as the pulse width time
            "PulseDelay": 0, #for use on the B2902B, defined as the delay time prior to pulse
            "PulseOffLevel": 0.0, #bias voltage during pulse-off
            # "Average": 1, # not yet supported
        }

        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']

        # not yet supported
        # self.average = int(parameter['Average'])
        #
        # if self.average < 1:
        #     self.average = 1
        # if self.average > 100:
        #     self.average = 100

        self.device = parameter['Device']
        self.channel = str(parameter['Channel'])[-1]
        
        self.pulse = parameter['CheckPulse'] #check in GUI is pulse option has been selected
        #self.pulse_meas_time_in_s = parameter['PulseMeasStart_in_s'] #for use on the B2902B, defined as the delay for measurement start after pulse release; variable not yet implemented
        self.ton = float(round(float(parameter["PulseOnTime"]), 6))  #for use on the B2902B, defined as the pulse width time
        self.toff = float(parameter["PulseDelay"]) #for use on the B2902B, defined as the delay time prior to pulse
        self.pulseofflevel = parameter['PulseOffLevel'] #bias voltage during pulse-off

    def initialize(self):
        # once at the beginning of the measurement
        
        #if float(self.pulse_meas_time_in_s) < 750e-6: #variable not yet implemented
        #    raise Exception("High voltage pulses can take as much as 750us to ramp up, measurement might start too early.\nPlease increase pulse measurement (start-)time or modify driver source code after validating your usecase with an Oscillopscope.")
        if float(self.ton) < 1e-3:
            raise Exception("Measurement at 0.01 NPLC requires at least 200us@50Hz PLC, pulse measurement start time is set to 750us minimum.\nThrefore, shortest pulse width is restricted to 1ms to ensure proper measurement at long ramp-up times of high current and voltage values.")
            
        self.port.write("*RST")

        self.port.write("SYST:BEEP:STAT OFF")  # control-Beep off

        self.port.write(":SYST:LFR 50")  # LineFrequency = 50 Hz

    def configure(self):

        if self.source.startswith("Voltage"):
            self.port.write(":SOUR%s:FUNC:MODE VOLT" % self.channel)
            # sourcemode = Voltage
            self.port.write(":SOUR%s:VOLT:MODE FIX" % self.channel)
            # sourcemode fix
            self.port.write(":SENS%s:FUNC \"CURR\"" % self.channel)
            # measurement mode
            self.port.write(":SENS%s:CURR:PROT %s" % (self.channel, self.protection))
            # Protection with Imax
            self.port.write(":SENS%s:CURR:RANG:AUTO ON" % self.channel)
            # Autorange for current measurement


        elif self.source.startswith("Current"):
            self.port.write(":SOUR%s:FUNC:MODE CURR" % self.channel)
            # sourcemode = Voltage
            self.port.write(":SOUR%s:CURR:MODE FIX" % self.channel)
            # sourcemode fix
            self.port.write(":SENS%s:FUNC \"VOLT\"" % self.channel)
            # measurement mode
            self.port.write(":SENS%s:VOLT:PROT %s" % (self.channel, self.protection))
            # Protection with Imax
            self.port.write(":SENS%s:VOLT:RANG:AUTO ON" % self.channel)
            # Autorange for voltage measurement

        if self.speed.startswith("Very Fast"): #newly impplemented to allow for measurements during fast pulses
            self.nplc = "0.01"
        elif self.speed.startswith("Fast"):
            self.nplc = "0.1"
        elif self.speed.startswith("Medium"):
            self.nplc = "1.0"
        elif self.speed.startswith("Slow"):
            self.nplc = "10.0"

        self.port.write(":SENS%s:CURR:NPLC %s" % (self.channel, self.nplc))
        self.port.write(":SENS%s:VOLT:NPLC %s" % (self.channel, self.nplc))

        self.port.write(":SENS%s:CURR:RANG:AUTO:MODE RES" % self.channel)

        # 4-wire sense
        if self.four_wire:
            self.port.write("SENS:REM ON")
        else:
            self.port.write("SENS:REM OFF")

        # Averaging is not yet supported but the below code might help to implement
        """
        # averaging
        self.port.write(":SENS:AVER:TCON REP")   # repeatedly take average
        if self.average > 1:
            self.port.write(":SENS:AVER ON") 
            self.port.write(":SENSe:AVER:COUN %i" % self.average)   # repeatedly take average
        else:
            self.port.write(":SENS:AVER OFF")
            self.port.write(":SENSe:AVER:COUN 1")  
        """

        if self.pulse:
            self.port.write(":FUNC PULS") #switch to pulse output instead of "DC"
            self.port.write(":PULS:WIDT %s" % self.ton) #pulse width time
            self.port.write(":PULS:DEL %s" % self.toff) #delay prior to pulse
            self.port.write(":SOUR%s:FUNC:TRIG:CONT OFF" % self.channel) #switch off continuous operation of internal trigger
            self.port.write(":SOUR%s:WAIT ON" % self.channel) #enables to wait for any change of amplitude past pulse
            self.port.write(":SENS%s:WAIT ON" % self.channel) #enables wait time for start of measurement defined by delay
            self.port.write(":TRIG%s:TRAN:DEL MIN" % self.channel) #trigger delay hardcoded to 0s
            self.port.write(":TRIG%s:ACQ:DEL 750e-6" % self.channel) #delay of measurement after pulse release is triggered; takes care of ramp-up
            self.port.write(":TRIG%s:ALL:COUN 1" % self.channel) #sets trigger count, 1 for single pulse
            self.port.write(":TRIG%s:LXI:LAN:DIS:ALL" % self.channel) #disable LXI triggering
            self.port.write(":TRIG%s:ALL:SOUR AINT" % self.channel) #enable internal trigger
            self.port.write(":TRIG%s:ALL:TIM MIN" % self.channel) #set trigger daly to minimum; not to be mixed up with pulse delay
            self.port.write(":FORM:ELEM:SENS VOLT,CURR,TIME,STAT,SOUR") #defining the measurement out sizes
            self.port.write(":SYST:TIME:TIM:COUN:RES:AUTO ON") #activates a counter timer reset
        else:
            self.port.write(":FUNC DC") #use traditional DC output instead of pulse

    def deinitialize(self):
        if self.four_wire:
            self.port.write("SYST:REM OFF")

        self.port.write(":SENS%s:CURR:NPLC 1" % self.channel)
        self.port.write(":SENS%s:VOLT:NPLC 1" % self.channel)

        # self.port.write(":SENS:AVER OFF")
        # self.port.write(":SENSe:AVER:COUN 1")

    def poweron(self):
        self.port.write(":OUTP%s ON" % self.channel)

    def poweroff(self):
        self.port.write(":OUTP%s OFF" % self.channel)

    def apply(self):

        value = str("{:.4E}".format(self.value)) #makes sure that self.value fits into SCPI command in terms of length
        if self.pulse:
            self.port.write(":SOUR%s:%s %s" % (self.channel, self.commands[self.source], self.pulseofflevel)) #get channel ready at specified values
            self.port.write(":SOUR%s:%s:TRIG %s" % (self.channel, self.commands[self.source], value)) #arming the pulse trigger
            self.port.write(":INIT (@%s)" % self.channel) #releasing the pulse trigger
        else:
            self.port.write(":SOUR%s:%s %s" % (self.channel, self.commands[self.source], value))  # set output to specified values

    def call(self):
    
        if self.pulse:
            self.port.write(":FETC:ARR? (@%s)" % self.channel) # get measured values taken during pulse release out of the memory
        else:
            self.port.write(":MEAS? (@%s)" % self.channel) #taking a measurement during constant DC output

        answer = self.port.read()

        values = answer.split(",")

        voltage = float(values[0])
        current = float(values[1])

        return [voltage, current]

    # here functions wrapping communication commands start

    def enable_output_protection(self, state: str | bool) -> None:
        """Enables  over voltage / over current protection.

        If the SMU hits the compliance the output will be switched off. This features is a safety feature to prevent
        further voltage or current being applied to the device under test.

        The output protection does not change whether the compliance is active.

        The default after a reset (*RST) of the device is OFF.
        """
        if state is True or state == "ON":
            state = "ON"
        elif state is False or state == "OFF":
            state = "OFF"
        else:
            msg = "State '%s' unknown and cannot be handled." % str(state)
            raise Exception(msg)

        self.port.write(f":OUTP{self.channel}:PROT {state}")

1 Like

Thanks Christian for the contribution! I will come back to you when the driver is available as testing version. It already looks quite nice.

Hi Axel.

Here is a slightly updated version which includes checks for compliance, because the SMU just outputs 0V if the combination of voltage and current is outside the operational cababilities.

# 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) 2024 SweepMe! GmbH (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: Agilent 29xx


from __future__ import annotations

from pysweepme.EmptyDeviceClass import EmptyDevice


class Device(EmptyDevice):

    def __init__(self):

        super().__init__()

        self.shortname = "Agilent B29xx"

        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 = ["USB", "GPIB"]

        self.port_properties = {
            # "timeout": 10,
        }

        self.commands = {
            "Voltage [V]": "VOLT",  # deprecated, remains for compatibility
            "Current [A]": "CURR",  # deprecated, remains for compatibility
            "Voltage in V": "VOLT",
            "Current in A": "CURR",
        }
        
        self.pulse_mode = True # enables pulsed signal option in GUI

    def set_GUIparameter(self):

        GUIparameter = {
            "SweepMode": ["Voltage in V", "Current in A"],
            "Channel": ["CH1", "CH2"],
            "4wire": False,
            # "RouteOut": ["Front", "Rear"],
            "Speed": ["Very Fast: 0.01NPLC", "Fast: 0.1NPLC", "Medium: 1NPLC", "Slow: 10NPLC"], # NPLCs included to transparently allow for user based measurement time estimations
            "Compliance": 100e-6,
            "CheckPulse": False,
            #"PulseMeasStart_in_s": 750e-6, #for use on the B2902B, defined as the delay for measurement start after pulse release;variable not yet implemented
            "PulseOnTime": 1e-3, #for use on the B2902B, defined as the pulse width time
            "PulseDelay": 0, #for use on the B2902B, defined as the delay time prior to pulse
            "PulseOffLevel": 0.0, #bias voltage during pulse-off
            # "Average": 1, # not yet supported
        }

        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']

        # not yet supported
        # self.average = int(parameter['Average'])
        #
        # if self.average < 1:
        #     self.average = 1
        # if self.average > 100:
        #     self.average = 100

        self.device = parameter['Device']
        self.channel = str(parameter['Channel'])[-1]
        
        self.pulse = parameter['CheckPulse'] #check in GUI is pulse option has been selected
        #self.pulse_meas_time_in_s = parameter['PulseMeasStart_in_s'] #for use on the B2902B, defined as the delay for measurement start after pulse release; variable not yet implemented
        self.ton = float(round(float(parameter["PulseOnTime"]), 6))  #for use on the B2902B, defined as the pulse width time
        self.toff = float(parameter["PulseDelay"]) #for use on the B2902B, defined as the delay time prior to pulse
        self.pulseofflevel = parameter['PulseOffLevel'] #bias voltage during pulse-off

    def initialize(self):
        # once at the beginning of the measurement
        
        #if float(self.pulse_meas_time_in_s) < 750e-6: #variable not yet implemented
        #    raise Exception("High voltage pulses can take as much as 750us to ramp up, measurement might start too early.\nPlease increase pulse measurement (start-)time or modify driver source code after validating your usecase with an Oscillopscope.")
        if float(self.ton) < 1e-3:
            raise Exception("Measurement at 0.01 NPLC requires at least 200us@50Hz PLC, pulse measurement start time is set to 750us minimum.\nThrefore, shortest pulse width is restricted to 1ms to ensure proper measurement at long ramp-up times of high current and voltage values.")
            
        self.port.write("*RST")

        self.port.write("SYST:BEEP:STAT OFF")  # control-Beep off

        self.port.write(":SYST:LFR 50")  # LineFrequency = 50 Hz

    def configure(self):

        if self.source.startswith("Voltage"):
            self.port.write(":SOUR%s:FUNC:MODE VOLT" % self.channel)
            # sourcemode = Voltage
            self.port.write(":SOUR%s:VOLT:MODE FIX" % self.channel)
            # sourcemode fix
            self.port.write(":SENS%s:FUNC \"CURR\"" % self.channel)
            # measurement mode
            self.port.write(":SENS%s:CURR:PROT %s" % (self.channel, self.protection))
            # Protection with Imax
            self.port.write(":SENS%s:CURR:RANG:AUTO ON" % self.channel)
            # Autorange for current measurement
            self.port.write(":SOUR%s:CURR:RANG:AUTO ON" % self.channel)
            # Autorange for current output


        elif self.source.startswith("Current"):
            self.port.write(":SOUR%s:FUNC:MODE CURR" % self.channel)
            # sourcemode = Voltage
            self.port.write(":SOUR%s:CURR:MODE FIX" % self.channel)
            # sourcemode fix
            self.port.write(":SENS%s:FUNC \"VOLT\"" % self.channel)
            # measurement mode
            self.port.write(":SENS%s:VOLT:PROT %s" % (self.channel, self.protection))
            # Protection with Imax
            self.port.write(":SENS%s:VOLT:RANG:AUTO ON" % self.channel)
            # Autorange for voltage measurement
            self.port.write(":SOUR%s:VOLT:RANG:AUTO ON" % self.channel)
            # Autorange for voltage output

        if self.speed.startswith("Very Fast"): #newly impplemented to allow for measurements during fast pulses
            self.nplc = "0.01"
        elif self.speed.startswith("Fast"):
            self.nplc = "0.1"
        elif self.speed.startswith("Medium"):
            self.nplc = "1.0"
        elif self.speed.startswith("Slow"):
            self.nplc = "10.0"

        self.port.write(":SENS%s:CURR:NPLC %s" % (self.channel, self.nplc))
        self.port.write(":SENS%s:VOLT:NPLC %s" % (self.channel, self.nplc))

        self.port.write(":SENS%s:CURR:RANG:AUTO:MODE RES" % self.channel)

        # 4-wire sense
        if self.four_wire:
            self.port.write("SENS:REM ON")
        else:
            self.port.write("SENS:REM OFF")

        # Averaging is not yet supported but the below code might help to implement
        """
        # averaging
        self.port.write(":SENS:AVER:TCON REP")   # repeatedly take average
        if self.average > 1:
            self.port.write(":SENS:AVER ON") 
            self.port.write(":SENSe:AVER:COUN %i" % self.average)   # repeatedly take average
        else:
            self.port.write(":SENS:AVER OFF")
            self.port.write(":SENSe:AVER:COUN 1")  
        """

        if self.pulse:
            self.port.write(":FUNC PULS") #switch to pulse output instead of "DC"
            self.port.write(":PULS:WIDT %s" % self.ton) #pulse width time
            self.port.write(":PULS:DEL %s" % self.toff) #delay prior to pulse
            self.port.write(":SOUR%s:FUNC:TRIG:CONT OFF" % self.channel) #switch off continuous operation of internal trigger
            self.port.write(":SOUR%s:WAIT ON" % self.channel) #enables to wait for any change of amplitude past pulse
            self.port.write(":SENS%s:WAIT ON" % self.channel) #enables wait time for start of measurement defined by delay
            self.port.write(":TRIG%s:TRAN:DEL MIN" % self.channel) #trigger delay hardcoded to 0s
            self.port.write(":TRIG%s:ACQ:DEL 750e-6" % self.channel) #delay of measurement after pulse release is triggered; takes care of ramp-up
            self.port.write(":TRIG%s:ALL:COUN 1" % self.channel) #sets trigger count, 1 for single pulse
            self.port.write(":TRIG%s:LXI:LAN:DIS:ALL" % self.channel) #disable LXI triggering
            self.port.write(":TRIG%s:ALL:SOUR AINT" % self.channel) #enable internal trigger
            self.port.write(":TRIG%s:ALL:TIM MIN" % self.channel) #set trigger daly to minimum; not to be mixed up with pulse delay
            self.port.write(":FORM:ELEM:SENS VOLT,CURR,TIME,STAT,SOUR") #defining the measurement out sizes
            self.port.write(":SYST:TIME:TIM:COUN:RES:AUTO ON") #activates a counter timer reset
        else:
            self.port.write(":FUNC DC") #std DC output

        self.port.write(":OUTP%s:LOW GRO" % self.channel) #FLOating or GROunded GND terminal

    def deinitialize(self):
        if self.four_wire:
            self.port.write("SYST:REM OFF")

        self.port.write(":SENS%s:CURR:NPLC 1" % self.channel)
        self.port.write(":SENS%s:VOLT:NPLC 1" % self.channel)

        # self.port.write(":SENS:AVER OFF")
        # self.port.write(":SENSe:AVER:COUN 1")

    def poweron(self):
        self.port.write(":OUTP%s ON" % self.channel)

    def poweroff(self):
        self.port.write(":OUTP%s OFF" % self.channel)

    def apply(self):
    
        if self.pulse:
            if self.source.startswith("Voltage"):
            
                if self.value > 200:
                    raise Exception("Voltage exceeding 200 V maximum pulse capability of device")
                if self.value < -200:
                    raise Exception("Voltage exceeding -200 V maximum pulse capability of device")
                    
                if float(self.protection) > 10.5:
                    raise Exception("Compliance above maximum pulse limit of 3.03 A")
                if float(self.protection) < -10.5:
                    raise Exception("Compliance below maximum pulse limit of -3.03 A")
                
                if float(self.protection) > 1.515 and self.value > 6:
                    raise Exception("Compliance above maximum limit of 1.515 A for voltages pulses above 6 V")
                if float(self.protection) < -1.515 and self.value < -6:
                    raise Exception("Compliance below maximum limit of -1.515 A for voltages pulses below -6 V")

            if self.source.startswith("Current"):
            
                if self.value > 10.5:
                    raise Exception("Compliance above maximum pulse limit of 3.03 A")
                if self.value < -10.5:
                    raise Exception("Compliance below maximum pulse limit of -3.03 A")
                
                if float(self.protection) > 200:
                    raise Exception("Voltage exceeding 200 V maximum pulse capability of device")
                if float(self.protection) < -200:
                    raise Exception("Voltage exceeding -200 V maximum pulse capability of device")
                
                if float(self.protection) > 6 and self.value > 1.515:
                    raise Exception("Compliance above maximum limit of 6 V for pulse currents above 1.515 A")
                if float(self.protection) < -6 and self.value < -1.515:
                    raise Exception("Compliance below maximum limit of -6 V for pulse currents below -1.515 A")
                    
        else:
            if self.source.startswith("Voltage"):
            
                if self.value > 210:
                    raise Exception("Voltage exceeding 210 V maximum capability of device")
                if self.value < -210:
                    raise Exception("Voltage exceeding -210 V maximum capability of device")
                
                if float(self.protection) > 3.03:
                    raise Exception("Compliance above maximum limit of 3.03 A")
                if float(self.protection) < -3.03:
                    raise Exception("Compliance below maximum limit of -3.03 A")
                
                if float(self.protection) > 1.515 and self.value > 6:
                    raise Exception("Compliance above maximum limit of 1.515 A for voltages above 6 V")
                if float(self.protection) < -1.515 and self.value < -6:
                    raise Exception("Compliance below maximum limit of -1.515 A for voltages below -6 V")
                
                if float(self.protection) > 0.105 and self.value > 21:
                    raise Exception("Compliance above maximum limit of 0.105 A for voltages above 21 V")
                if float(self.protection) < -0.105 and self.value < -21:
                    raise Exception("Compliance below maximum limit of -0.105 A for voltages below -21 V")
            
            if self.source.startswith("Current"):
            
                if self.value > 3.03:
                    raise Exception("Voltage exceeding 3.03 A maximum capability of device")
                if self.value < -3.03:
                    raise Exception("Voltage exceeding -3.03 A maximum capability of device")
                
                if float(self.protection) > 210:
                    raise Exception("Compliance above maximum limit of 210 V")
                if float(self.protection) < -210:
                    raise Exception("Compliance below maximum limit of -210 V")
                
                if float(self.protection) > 21 and self.value > 0.105:
                    raise Exception("Compliance above maximum limit of 21 V for currents above 0.105 A")
                if float(self.protection) < -21 and self.value < -0.105:
                    raise Exception("Compliance below maximum limit of -21 V for currents below -0.105 A")
                
                if float(self.protection) > 6 and self.value > 1.515:
                    raise Exception("Compliance above maximum limit of 6 V for currents above 1.515 A")
                if float(self.protection) < -6 and self.value < -1.515:
                    raise Exception("Compliance below maximum limit of -6 V for currents above -1.515 A")
                    
        value = str("{:.4E}".format(self.value)) #makes sure that self.value fits into SCPI command in terms of length
        if self.pulse:
            self.port.write(":SOUR%s:%s %s" % (self.channel, self.commands[self.source], self.pulseofflevel)) #get channel ready at specified values
            self.port.write(":SOUR%s:%s:TRIG %s" % (self.channel, self.commands[self.source], value)) #arming the pulse trigger
            self.port.write(":INIT (@%s)" % self.channel) #releasing the pulse trigger
        else:
            self.port.write(":SOUR%s:%s %s" % (self.channel, self.commands[self.source], value))  # set output to specified values

    def call(self):
    
        if self.pulse:
            self.port.write(":FETC:ARR? (@%s)" % self.channel) # get measured values taken during pulse release out of the memory
        else:
            self.port.write(":MEAS? (@%s)" % self.channel) #taking a measurement during constant DC output

        answer = self.port.read()

        values = answer.split(",")

        voltage = float(values[0])
        current = float(values[1])
        
        return [voltage, current]

    # here functions wrapping communication commands start

    def enable_output_protection(self, state: str | bool) -> None:
        """Enables  over voltage / over current protection.

        If the SMU hits the compliance the output will be switched off. This features is a safety feature to prevent
        further voltage or current being applied to the device under test.

        The output protection does not change whether the compliance is active.

        The default after a reset (*RST) of the device is OFF.
        """
        if state is True or state == "ON":
            state = "ON"
        elif state is False or state == "OFF":
            state = "OFF"
        else:
            msg = "State '%s' unknown and cannot be handled." % str(state)
            raise Exception(msg)

        self.port.write(f":OUTP{self.channel}:PROT {state}")

Best wishes,
Christian

1 Like

Addendum: the pulse delay value seems to cause a conflict with the aquire trigger. I need to have a second look. Until then, we should hardcore the trigger delay to 0.

Hi Christian, do you like to make further modifications or shall I include the trigger delay of 0 into your latest version.

So far all changes are reasonable. I was only wondering whether the range limitations that you introduced for pulsing are related to your model or to all SMUs of the B2900 series with pulse capabilities.

Let me know whether I can proceed and I will provide a new version for further testing.

Hi Axel.

I seem to have found a solution already. At least it makes sense and the SMU doesn’t throw an error anymore. Please stand by until Thursday, I would like to run additional measurements before updating the driver here again.

Best wishes,
Christian

1 Like

Hello Axel.

This version seems to work and should be used for review. The problems I saw in use were caused by the pulse delay time. The SCPI handbook explains the required parameters as follows:

A closer look reveals that any value greater than 0 for the parameter :PULSe:DELay requires the parameter :TRIGger:ACQuire:DELay to be adjusted accordingly to maintain its position relative to the pulse. Otherwise measurement results would not be consistent and in cases with large pulse delays like multiple ms and short pulse durations, the SMU would try to trigger the measurement after the pulse has already completed, leading to an error.

Therefore, a new variable was introduced called self.acqdelay that is used for the TRIG:ACQ:DEL parameter in the SCPI command that computes out of the pulse delay time self.toff and the hardcoded minimum value of 750us. This seems to do the trick.

# 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) 2024 SweepMe! GmbH (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: Agilent 29xx


from __future__ import annotations

from pysweepme.EmptyDeviceClass import EmptyDevice


class Device(EmptyDevice):

    def __init__(self):

        super().__init__()

        self.shortname = "Agilent B29xx"

        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 = ["USB", "GPIB"]

        self.port_properties = {
            # "timeout": 10,
        }

        self.commands = {
            "Voltage [V]": "VOLT",  # deprecated, remains for compatibility
            "Current [A]": "CURR",  # deprecated, remains for compatibility
            "Voltage in V": "VOLT",
            "Current in A": "CURR",
        }
        
        self.pulse_mode = True # enables pulsed signal option in GUI

    def set_GUIparameter(self):

        GUIparameter = {
            "SweepMode": ["Voltage in V", "Current in A"],
            "Channel": ["CH1", "CH2"],
            "4wire": False,
            # "RouteOut": ["Front", "Rear"],
            "Speed": ["Very Fast: 0.01NPLC", "Fast: 0.1NPLC", "Medium: 1NPLC", "Slow: 10NPLC"], # NPLCs included to transparently allow for user based measurement time estimations
            "Compliance": 100e-6,
            "CheckPulse": False,
            #"PulseMeasStart_in_s": 750e-6, #for use on the B2902B, defined as the delay for measurement start after pulse release;variable not yet implemented
            "PulseOnTime": 1e-3, #for use on the B2902B, defined as the pulse width time
            "PulseDelay": 0, #for use on the B2902B, defined as the delay time prior to pulse
            "PulseOffLevel": 0.0, #bias voltage during pulse-off
            # "Average": 1, # not yet supported
        }

        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']

        # not yet supported
        # self.average = int(parameter['Average'])
        #
        # if self.average < 1:
        #     self.average = 1
        # if self.average > 100:
        #     self.average = 100

        self.device = parameter['Device']
        self.channel = str(parameter['Channel'])[-1]
        
        self.pulse = parameter['CheckPulse'] #check in GUI is pulse option has been selected
        #self.pulse_meas_time_in_s = parameter['PulseMeasStart_in_s'] #for use on the B2902B, defined as the delay for measurement start after pulse release; variable not yet implemented
        self.ton = float(round(float(parameter["PulseOnTime"]), 6))  #for use on the B2902B, defined as the pulse width time
        self.toff = float(parameter["PulseDelay"]) #for use on the B2902B, defined as the delay time prior to pulse
        self.pulseofflevel = parameter['PulseOffLevel'] #bias voltage during pulse-off
        self.acqdelay=str("{:.4E}".format(self.toff+750e-6)) #pulse delay is part of trigger acquire delay; this equation makes sure than the minimum acquire delay of 750us is extended by the amount of pulse delay requested by the user via the GUI
        print(self.acqdelay)
    def initialize(self):
        # once at the beginning of the measurement
        
        #if float(self.pulse_meas_time_in_s) < 750e-6: #variable not yet implemented
        #    raise Exception("High voltage pulses can take as much as 750us to ramp up, measurement might start too early.\nPlease increase pulse measurement (start-)time or modify driver source code after validating your usecase with an Oscillopscope.")
        if float(self.ton) < 1e-3:
            raise Exception("Measurement at 0.01 NPLC requires at least 200us@50Hz PLC, pulse measurement start time is set to 750us minimum.\nThrefore, shortest pulse width is restricted to 1ms to ensure proper measurement at long ramp-up times of high current and voltage values.")
            
        self.port.write("*RST")

        self.port.write("SYST:BEEP:STAT OFF")  # control-Beep off

        self.port.write(":SYST:LFR 50")  # LineFrequency = 50 Hz

    def configure(self):

        if self.source.startswith("Voltage"):
            self.port.write(":SOUR%s:FUNC:MODE VOLT" % self.channel)
            # sourcemode = Voltage
            self.port.write(":SOUR%s:VOLT:MODE FIX" % self.channel)
            # sourcemode fix
            self.port.write(":SENS%s:FUNC \"CURR\"" % self.channel)
            # measurement mode
            self.port.write(":SENS%s:CURR:PROT %s" % (self.channel, self.protection))
            # Protection with Imax
            self.port.write(":SENS%s:CURR:RANG:AUTO ON" % self.channel)
            # Autorange for current measurement
            self.port.write(":SOUR%s:CURR:RANG:AUTO ON" % self.channel)
            # Autorange for current output


        elif self.source.startswith("Current"):
            self.port.write(":SOUR%s:FUNC:MODE CURR" % self.channel)
            # sourcemode = Voltage
            self.port.write(":SOUR%s:CURR:MODE FIX" % self.channel)
            # sourcemode fix
            self.port.write(":SENS%s:FUNC \"VOLT\"" % self.channel)
            # measurement mode
            self.port.write(":SENS%s:VOLT:PROT %s" % (self.channel, self.protection))
            # Protection with Imax
            self.port.write(":SENS%s:VOLT:RANG:AUTO ON" % self.channel)
            # Autorange for voltage measurement
            self.port.write(":SOUR%s:VOLT:RANG:AUTO ON" % self.channel)
            # Autorange for voltage output

        if self.speed.startswith("Very Fast"): #newly impplemented to allow for measurements during fast pulses
            self.nplc = "0.01"
        elif self.speed.startswith("Fast"):
            self.nplc = "0.1"
        elif self.speed.startswith("Medium"):
            self.nplc = "1.0"
        elif self.speed.startswith("Slow"):
            self.nplc = "10.0"

        self.port.write(":SENS%s:CURR:NPLC %s" % (self.channel, self.nplc))
        self.port.write(":SENS%s:VOLT:NPLC %s" % (self.channel, self.nplc))

        self.port.write(":SENS%s:CURR:RANG:AUTO:MODE RES" % self.channel)

        # 4-wire sense
        if self.four_wire:
            self.port.write("SENS:REM ON")
        else:
            self.port.write("SENS:REM OFF")

        # Averaging is not yet supported but the below code might help to implement
        """
        # averaging
        self.port.write(":SENS:AVER:TCON REP")   # repeatedly take average
        if self.average > 1:
            self.port.write(":SENS:AVER ON") 
            self.port.write(":SENSe:AVER:COUN %i" % self.average)   # repeatedly take average
        else:
            self.port.write(":SENS:AVER OFF")
            self.port.write(":SENSe:AVER:COUN 1")  
        """

        if self.pulse:
            self.port.write(":FUNC PULS") #switch to pulse output instead of "DC"
            self.port.write(":PULS:WIDT %s" % self.ton) #pulse width time
            self.port.write(":PULS:DEL %s" % self.toff) #delay prior to pulse
            self.port.write(":SOUR%s:FUNC:TRIG:CONT OFF" % self.channel) #switch off continuous operation of internal trigger
            self.port.write(":SOUR%s:WAIT ON" % self.channel) #enables to wait for any change of amplitude past pulse
            self.port.write(":SENS%s:WAIT ON" % self.channel) #enables wait time for start of measurement defined by delay
            self.port.write(":TRIG%s:TRAN:DEL MIN" % self.channel) #trigger delay hardcoded to 0s
            self.port.write(":TRIG%s:ACQ:DEL %s" % (self.channel, self.acqdelay)) #delay of measurement after pulse release is triggered; takes care of ramp-up
            self.port.write(":TRIG%s:ALL:COUN 1" % self.channel) #sets trigger count, 1 for single pulse
            self.port.write(":TRIG%s:LXI:LAN:DIS:ALL" % self.channel) #disable LXI triggering
            self.port.write(":TRIG%s:ALL:SOUR AINT" % self.channel) #enable internal trigger
            self.port.write(":TRIG%s:ALL:TIM MIN" % self.channel) #set trigger daly to minimum; not to be mixed up with pulse delay
            self.port.write(":FORM:ELEM:SENS VOLT,CURR,TIME,STAT,SOUR") #defining the measurement out sizes
            self.port.write(":SYST:TIME:TIM:COUN:RES:AUTO ON") #activates a counter timer reset
        else:
            self.port.write(":FUNC DC") #std DC output

        self.port.write(":OUTP%s:LOW GRO" % self.channel) #FLOating or GROunded GND terminal

    def deinitialize(self):
        if self.four_wire:
            self.port.write("SYST:REM OFF")

        self.port.write(":SENS%s:CURR:NPLC 1" % self.channel)
        self.port.write(":SENS%s:VOLT:NPLC 1" % self.channel)

        # self.port.write(":SENS:AVER OFF")
        # self.port.write(":SENSe:AVER:COUN 1")

    def poweron(self):
        self.port.write(":OUTP%s ON" % self.channel)

    def poweroff(self):
        self.port.write(":OUTP%s OFF" % self.channel)

    def apply(self):
    
        if self.pulse:
            if self.source.startswith("Voltage"):
            
                if self.value > 200:
                    raise Exception("Voltage exceeding 200 V maximum pulse capability of device")
                if self.value < -200:
                    raise Exception("Voltage exceeding -200 V maximum pulse capability of device")
                    
                if float(self.protection) > 10.5:
                    raise Exception("Compliance above maximum pulse limit of 3.03 A")
                if float(self.protection) < -10.5:
                    raise Exception("Compliance below maximum pulse limit of -3.03 A")
                
                if float(self.protection) > 1.515 and self.value > 6:
                    raise Exception("Compliance above maximum limit of 1.515 A for voltages pulses above 6 V")
                if float(self.protection) < -1.515 and self.value < -6:
                    raise Exception("Compliance below maximum limit of -1.515 A for voltages pulses below -6 V")

            if self.source.startswith("Current"):
            
                if self.value > 10.5:
                    raise Exception("Compliance above maximum pulse limit of 3.03 A")
                if self.value < -10.5:
                    raise Exception("Compliance below maximum pulse limit of -3.03 A")
                
                if float(self.protection) > 200:
                    raise Exception("Voltage exceeding 200 V maximum pulse capability of device")
                if float(self.protection) < -200:
                    raise Exception("Voltage exceeding -200 V maximum pulse capability of device")
                
                if float(self.protection) > 6 and self.value > 1.515:
                    raise Exception("Compliance above maximum limit of 6 V for pulse currents above 1.515 A")
                if float(self.protection) < -6 and self.value < -1.515:
                    raise Exception("Compliance below maximum limit of -6 V for pulse currents below -1.515 A")
                    
        else:
            if self.source.startswith("Voltage"):
            
                if self.value > 210:
                    raise Exception("Voltage exceeding 210 V maximum capability of device")
                if self.value < -210:
                    raise Exception("Voltage exceeding -210 V maximum capability of device")
                
                if float(self.protection) > 3.03:
                    raise Exception("Compliance above maximum limit of 3.03 A")
                if float(self.protection) < -3.03:
                    raise Exception("Compliance below maximum limit of -3.03 A")
                
                if float(self.protection) > 1.515 and self.value > 6:
                    raise Exception("Compliance above maximum limit of 1.515 A for voltages above 6 V")
                if float(self.protection) < -1.515 and self.value < -6:
                    raise Exception("Compliance below maximum limit of -1.515 A for voltages below -6 V")
                
                if float(self.protection) > 0.105 and self.value > 21:
                    raise Exception("Compliance above maximum limit of 0.105 A for voltages above 21 V")
                if float(self.protection) < -0.105 and self.value < -21:
                    raise Exception("Compliance below maximum limit of -0.105 A for voltages below -21 V")
            
            if self.source.startswith("Current"):
            
                if self.value > 3.03:
                    raise Exception("Voltage exceeding 3.03 A maximum capability of device")
                if self.value < -3.03:
                    raise Exception("Voltage exceeding -3.03 A maximum capability of device")
                
                if float(self.protection) > 210:
                    raise Exception("Compliance above maximum limit of 210 V")
                if float(self.protection) < -210:
                    raise Exception("Compliance below maximum limit of -210 V")
                
                if float(self.protection) > 21 and self.value > 0.105:
                    raise Exception("Compliance above maximum limit of 21 V for currents above 0.105 A")
                if float(self.protection) < -21 and self.value < -0.105:
                    raise Exception("Compliance below maximum limit of -21 V for currents below -0.105 A")
                
                if float(self.protection) > 6 and self.value > 1.515:
                    raise Exception("Compliance above maximum limit of 6 V for currents above 1.515 A")
                if float(self.protection) < -6 and self.value < -1.515:
                    raise Exception("Compliance below maximum limit of -6 V for currents above -1.515 A")
                    
        value = str("{:.4E}".format(self.value)) #makes sure that self.value fits into SCPI command in terms of length
        if self.pulse:
            self.port.write(":SOUR%s:%s %s" % (self.channel, self.commands[self.source], self.pulseofflevel)) #get channel ready at specified values
            self.port.write(":SOUR%s:%s:TRIG %s" % (self.channel, self.commands[self.source], value)) #arming the pulse trigger
            self.port.write(":INIT (@%s)" % self.channel) #releasing the pulse trigger
        else:
            self.port.write(":SOUR%s:%s %s" % (self.channel, self.commands[self.source], value))  # set output to specified values

    def call(self):
    
        if self.pulse:
            self.port.write(":FETC:ARR? (@%s)" % self.channel) # get measured values taken during pulse release out of the memory
        else:
            self.port.write(":MEAS? (@%s)" % self.channel) #taking a measurement during constant DC output

        answer = self.port.read()

        values = answer.split(",")

        voltage = float(values[0])
        current = float(values[1])
        
        return [voltage, current]

    # here functions wrapping communication commands start

    def enable_output_protection(self, state: str | bool) -> None:
        """Enables  over voltage / over current protection.

        If the SMU hits the compliance the output will be switched off. This features is a safety feature to prevent
        further voltage or current being applied to the device under test.

        The output protection does not change whether the compliance is active.

        The default after a reset (*RST) of the device is OFF.
        """
        if state is True or state == "ON":
            state = "ON"
        elif state is False or state == "OFF":
            state = "OFF"
        else:
            msg = "State '%s' unknown and cannot be handled." % str(state)
            raise Exception(msg)

        self.port.write(f":OUTP{self.channel}:PROT {state}")

Best wishes,
Christian

1 Like

Thanks Christian, for the deep dive into the pulse mode of the B2900 series.

I will now add the new version to the repository and release an update. Please use this new version for further changes.

I will write again once the new version is available.

1 Like

I went through the driver and just did some style changes. For example I changed “Very Fast: …” to “Very fast: …” in the speed/integration option.

Others changes are mainly regarding line formatting and raising Exceptions via an additional msg variable which makes the debug output a bit nicer.

You can see all changes here:

The new driver is uploaded as testing version (2024-07-05). You can download it after login in SweepMe!. If you verify it works, I will make the new version available to everyone.

Thanks again for your very nice addition to the B29xx driver!

My colleague Felix has checked the code of the driver as well and made some comments at the pull request:

Before I change something, please have a look and tell me whether you like to do some changes. Otherwise, I can also incorporate the feedback and provide a new version.

If you are interested, it would be also possible that you can contribute drivers directly via Github where we can work on the drivers together.

I’ve registered with Github and posted replies to Felix’ comments. Let’s see if he agrees.

1 Like

Thanks, I will go through the driver and push some changes.

Basically, we could move further driver contributions to GitHub now.
For example you can clone the repository, create a new branch, make a commit for a new driver, and we add it after a revision phase of your pull request.

If you have questions about that feel free to contact me or open a new discussion here:

It would be also possible to contribute a driver by opening a discussion on Github without pushing the code directly if you prefer this.

As the forum is relatively new, we are about to adjust which contents are handled where. I think the driver contributions which is already a developer topic could be handled best in GitHub while questions about the program usage where people use ready-made drivers and module fit well to the forum.

Feel free to share your oppinion.

1 Like

I think a good solution would be to use this forum to “annouce” drivers that have been added to the git repro and use it for further anouncements about updates as well as offering it for questions regarding its use. This way, users without programming/customizing ambitions are kept informed about its progress and the forum represents an added benefit for them. The forum would be rather the center for users and github the one for developers.

Following along this proposal, personally, I would follow your suggestion to clone, branch and commit on github and post a notice to this board about the new driver proposal.

I have to admit that I have been using forums for various topics of dicussion for around 25 years now which is why I tend to fall back to this kind of discussion plattform naturally. But github is probably the better choice for developement.

1 Like

Good summary of how we should do it :slight_smile:

1 Like

Hi Christian,

there is one open question regarding the pull request:

Do you like to have a look? Not sure whether you got an info from GitHub already.

Thanks and best, Axel