#!/usr/bin/env python
#
# hp3456a.py: Driver for the HP3456a Digital Voltmeter.
#
# © 2014 Willem Dijkstra (wpd@xs4all.nl).
#
# This file is a part of the InstrumentKit project.
# Licensed under the AGPL version 3.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Driver for the HP3456a Digital Voltmeter
Originally contributed and copyright held by Willem Dijkstra (wpd@xs4all.nl)
An unrestricted license has been provided to the maintainers of the Instrument
Kit project.
"""
# IMPORTS #####################################################################
import time
from enum import Enum, IntEnum
from instruments.abstract_instruments import Multimeter
from instruments.units import ureg as u
from instruments.util_fns import assume_units, bool_property, enum_property
# CLASSES #####################################################################
[docs]
class HP3456a(Multimeter):
"""The `HP3456a` is a 6 1/2 digit bench multimeter.
It supports DCV, ACV, ACV + DCV, 2 wire Ohms, 4 wire Ohms, DCV/DCV Ratio,
ACV/DCV Ratio, Offset compensated 2 wire Ohms and Offset compensated 4 wire
Ohms measurements.
Measurements can be further extended using a system math mode that allows
for pass/fail, statistics, dB/dBm, null, scale and percentage readings.
`HP3456a` is a HPIB / pre-448.2 instrument.
"""
def __init__(self, filelike):
"""
Initialise the instrument, and set the required eos, eoi needed for
communication.
"""
super().__init__(filelike)
self.timeout = 15 * u.second
self.terminator = "\r"
self.sendcmd("HO0T4SO1")
self._null = False
# ENUMS ##
[docs]
class MathMode(IntEnum):
"""
Enum with the supported math modes
"""
off = 0
pass_fail = 1
statistic = 2
null = 3
dbm = 4
thermistor_f = 5
thermistor_c = 6
scale = 7
percent = 8
db = 9
[docs]
class Mode(Enum):
"""
Enum containing the supported mode codes
"""
#: DC voltage
dcv = "S0F1"
#: AC voltage
acv = "S0F2"
#: RMS of DC + AC voltage
acvdcv = "S0F3"
#: 2 wire resistance
resistance_2wire = "S0F4"
#: 4 wire resistance
resistance_4wire = "S0F5"
#: ratio DC / DC voltage
ratio_dcv_dcv = "S1F1"
#: ratio AC / DC voltage
ratio_acv_dcv = "S1F2"
#: ratio (AC + DC) / DC voltage
ratio_acvdcv_dcv = "S1F3"
#: offset compensated 2 wire resistance
oc_resistence_2wire = "S1F4"
#: offset compensated 4 wire resistance
oc_resistence_4wire = "S1F5"
[docs]
class Register(Enum):
"""
Enum with the register names for all `HP3456a` internal registers.
"""
number_of_readings = "N"
number_of_digits = "G"
nplc = "I"
delay = "D"
mean = "M"
variance = "V"
count = "C"
lower = "L"
r = "R"
upper = "U"
y = "Y"
z = "Z"
[docs]
class TriggerMode(IntEnum):
"""
Enum with valid trigger modes.
"""
internal = 1
external = 2
single = 3
hold = 4
[docs]
class ValidRange(Enum):
"""
Enum with the valid ranges for voltage, resistance, and number of
powerline cycles to integrate over.
"""
voltage = (1e-1, 1e0, 1e1, 1e2, 1e3)
resistance = (1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9)
nplc = (1e-1, 1e0, 1e1, 1e2)
# PROPERTIES ##
mode = enum_property(
"",
Mode,
doc="""Set the measurement mode.
:type: `HP3456a.Mode`
""",
writeonly=True,
set_fmt="{}{}",
)
autozero = bool_property(
"Z",
inst_true="1",
inst_false="0",
doc="""Set the autozero mode.
This is used to compensate for offsets in the dc
input amplifier circuit of the multimeter. If set, the amplifier"s input
circuit is shorted to ground prior to actual measurement in order to
take an offset reading. This offset is then used to compensate for
drift in the next measurement. When disabled, one offset reading
is taken immediately and stored into memory to be used for all
successive measurements onwards. Disabling autozero increases the
`HP3456a`"s measurement speed, and also makes the instrument more
suitable for high impendance measurements since no input switching is
done.""",
writeonly=True,
set_fmt="{}{}",
)
filter = bool_property(
"FL",
inst_true="1",
inst_false="0",
doc="""Set the analog filter mode.
The `HP3456a` has a 3 pole active filter with
greater than 60dB attenuation at frequencies of 50Hz and higher. The
filter is applied between the input terminals and input amplifier. When
in ACV or ACV+DCV functions the filter is applied to the output of the
ac converter and input amplifier. In these modes select the filter for
measurements below 400Hz.""",
writeonly=True,
set_fmt="{}{}",
)
math_mode = enum_property(
"M",
MathMode,
doc="""Set the math mode.
The `HP3456a` has a number of different math modes that
can change measurement output, or can provide additional
statistics. Interaction with these modes is done via the
`HP3456a.Register`.
:type: `HP3456a.MathMode`
""",
writeonly=True,
set_fmt="{}{}",
)
trigger_mode = enum_property(
"T",
TriggerMode,
doc="""Set the trigger mode.
Note that using `HP3456a.measure()` will override the `trigger_mode` to
`HP3456a.TriggerMode.single`.
:type: `HP3456a.TriggerMode`
""",
writeonly=True,
set_fmt="{}{}",
)
@property
def number_of_readings(self):
"""Get/set the number of readings done per trigger/measurement cycle
using `HP3456a.Register.number_of_readings`.
:type: `float`
:rtype: `float`
"""
return self._register_read(HP3456a.Register.number_of_readings)
@number_of_readings.setter
def number_of_readings(self, value):
self._register_write(HP3456a.Register.number_of_readings, value)
@property
def number_of_digits(self):
"""Get/set the number of digits used in measurements using
`HP3456a.Register.number_of_digits`.
Set to higher values to increase accuracy at the cost of measurement
speed.
:type: `int`
"""
return int(self._register_read(HP3456a.Register.number_of_digits))
@number_of_digits.setter
def number_of_digits(self, newval):
newval = int(newval)
if newval not in range(3, 7):
raise ValueError(
"Valid number_of_digits are: " "{}".format(list(range(3, 7)))
)
self._register_write(HP3456a.Register.number_of_digits, newval)
@property
def nplc(self):
"""Get/set the number of powerline cycles to integrate per measurement
using `HP3456a.Register.nplc`.
Setting higher values increases accuracy at the cost of a longer
measurement time. The implicit assumption is that the input reading is
stable over the number of powerline cycles to integrate.
:type: `int`
"""
return int(self._register_read(HP3456a.Register.nplc))
@nplc.setter
def nplc(self, newval):
newval = int(newval)
valid = HP3456a.ValidRange["nplc"].value
if newval in valid:
self._register_write(HP3456a.Register.nplc, newval)
else:
raise ValueError("Valid nplc settings are: " "{}".format(valid))
@property
def delay(self):
"""Get/set the delay that is waited after a trigger for the input to
settle using `HP3456a.Register.delay`.
:type: As specified, assumed to be `~quantaties.Quantity.s` otherwise
:rtype: `~quantaties.Quantity.s`
"""
return self._register_read(HP3456a.Register.delay) * u.s
@delay.setter
def delay(self, value):
delay = assume_units(value, u.s).to(u.s).magnitude
self._register_write(HP3456a.Register.delay, delay)
@property
def mean(self):
"""
Get the mean over `HP3456a.Register.count` measurements from
`HP3456a.Register.mean` when in `HP3456a.MathMode.statistic`.
:rtype: `float`
"""
return self._register_read(HP3456a.Register.mean)
@property
def variance(self):
"""
Get the variance over `HP3456a.Register.count` measurements from
`HP3456a.Register.variance` when in `HP3456a.MathMode.statistic`.
:rtype: `float`
"""
return self._register_read(HP3456a.Register.variance)
@property
def count(self):
"""
Get the number of measurements taken from `HP3456a.Register.count` when
in `HP3456a.MathMode.statistic`.
:rtype: `int`
"""
return int(self._register_read(HP3456a.Register.count))
@property
def lower(self):
"""
Get/set the value in `HP3456a.Register.lower`, which indicates the
lowest value measurement made while in `HP3456a.MathMode.statistic`, or
the lowest value preset for `HP3456a.MathMode.pass_fail`.
:type: `float`
"""
return self._register_read(HP3456a.Register.lower)
@lower.setter
def lower(self, value):
self._register_write(HP3456a.Register.lower, value)
@property
def upper(self):
"""
Get/set the value in `HP3456a.Register.upper`, which indicates the
highest value measurement made while in `HP3456a.MathMode.statistic`,
or the highest value preset for `HP3456a.MathMode.pass_fail`.
:type: `float`
:rtype: `float`
"""
return self._register_read(HP3456a.Register.upper)
@upper.setter
def upper(self, value):
return self._register_write(HP3456a.Register.upper, value)
@property
def r(self):
"""
Get/set the value in `HP3456a.Register.r`, which indicates the resistor
value used while in `HP3456a.MathMode.dbm` or the number of recalled
readings in reading storage mode.
:type: `float`
:rtype: `float`
"""
return self._register_read(HP3456a.Register.r)
@r.setter
def r(self, value):
self._register_write(HP3456a.Register.r, value)
@property
def y(self):
"""
Get/set the value in `HP3456a.Register.y` to be used in calculations
when in `HP3456a.MathMode.scale` or `HP3456a.MathMode.percent`.
:type: `float`
:rtype: `float`
"""
return self._register_read(HP3456a.Register.y)
@y.setter
def y(self, value):
self._register_write(HP3456a.Register.y, value)
@property
def z(self):
"""
Get/set the value in `HP3456a.Register.z` to be used in calculations
when in `HP3456a.MathMode.scale` or the first reading when in
`HP3456a.MathMode.statistic`.
:type: `float`
:rtype: `float`
"""
return self._register_read(HP3456a.Register.z)
@z.setter
def z(self, value):
self._register_write(HP3456a.Register.z, value)
@property
def input_range(self):
"""Set the input range to be used.
The `HP3456a` has separate ranges for `ohm` and for
`volt`. The range value sent to the instrument depends on
the unit set on the input range value. `auto` selects auto ranging.
:type: `~pint.Quantity`
"""
raise NotImplementedError
@input_range.setter
def input_range(self, value):
if isinstance(value, str):
if value.lower() == "auto":
self.sendcmd("R1W")
else:
raise ValueError(
"Only 'auto' is acceptable when specifying "
"the input range as a string."
)
elif isinstance(value, u.Quantity):
if value.units == u.volt:
valid = HP3456a.ValidRange.voltage.value
value = value.to(u.volt)
elif value.units == u.ohm:
valid = HP3456a.ValidRange.resistance.value
value = value.to(u.ohm)
else:
raise ValueError(
"Value {} not quantity.volt or quantity.ohm" "".format(value)
)
value = float(value.magnitude)
if value not in valid:
raise ValueError(
"Value {} outside valid ranges " "{}".format(value, valid)
)
value = valid.index(value) + 2
self.sendcmd(f"R{value}W")
else:
raise TypeError(
"Range setting must be specified as a float, int, "
"or the string 'auto', got {}".format(type(value))
)
@property
def relative(self):
"""
Enable or disable `HP3456a.MathMode.Null` on the instrument.
:type: `bool`
"""
return self._null
@relative.setter
def relative(self, value):
if value is True:
self._null = True
self.sendcmd(f"M{HP3456a.MathMode.null.value}")
elif value is False:
self._null = False
self.sendcmd(f"M{HP3456a.MathMode.off.value}")
else:
raise TypeError(
"Relative setting must be specified as a bool, "
"got {}".format(type(value))
)
# METHODS ##
[docs]
def auto_range(self):
"""
Set input range to auto. The `HP3456a` should upscale when a reading
is at 120% and downscale when it below 11% full scale. Note that auto
ranging can increase the measurement time.
"""
self.input_range = "auto"
[docs]
def fetch(self, mode=None):
"""Retrieve n measurements after the HP3456a has been instructed to
perform a series of similar measurements. Typically the mode, range,
nplc, analog filter, autozero is set along with the number of
measurements to take. The series is then started at the trigger
command.
Example usage:
>>> dmm.number_of_digits = 6
>>> dmm.auto_range()
>>> dmm.nplc = 1
>>> dmm.mode = dmm.Mode.resistance_2wire
>>> n = 100
>>> dmm.number_of_readings = n
>>> dmm.trigger()
>>> time.sleep(n * 0.04)
>>> v = dmm.fetch(dmm.Mode.resistance_2wire)
>>> print len(v)
10
:param mode: Desired measurement mode. If not specified, the previous
set mode will be used, but no measurement unit will be returned.
:type mode: `HP3456a.Mode`
:return: A series of measurements from the multimeter.
:rtype: `~pint.Quantity`
"""
if mode is not None:
units = UNITS[mode]
else:
units = 1
value = self.query("", size=-1)
values = [float(x) * units for x in value.split(",")]
return values
[docs]
def measure(self, mode=None):
"""Instruct the HP3456a to perform a one time measurement. The
measurement will use the current set registers for the measurement
(number_of_readings, number_of_digits, nplc, delay, mean, lower, upper,
y and z) and will immediately take place.
Note that using `HP3456a.measure()` will override the `trigger_mode` to
`HP3456a.TriggerMode.single`
Example usage:
>>> dmm = ik.hp.HP3456a.open_gpibusb("/dev/ttyUSB0", 22)
>>> dmm.number_of_digits = 6
>>> dmm.nplc = 1
>>> print dmm.measure(dmm.Mode.resistance_2wire)
:param mode: Desired measurement mode. If not specified, the previous
set mode will be used, but no measurement unit will be
returned.
:type mode: `HP3456a.Mode`
:return: A measurement from the multimeter.
:rtype: `~pint.Quantity`
"""
if mode is not None:
modevalue = mode.value
units = UNITS[mode]
else:
modevalue = ""
units = 1
self.sendcmd(f"{modevalue}W1STNT3")
value = self.query("", size=-1)
return float(value) * units
def _register_read(self, name):
"""
Read a register on the HP3456a.
:param name: The name of the register to read from
:type name: `HP3456a.Register`
:rtype: `float`
"""
try:
name = HP3456a.Register[name]
except KeyError:
pass
if not isinstance(name, HP3456a.Register):
raise TypeError(
"register must be specified as a "
"HP3456a.Register, got {} "
"instead.".format(name)
)
self.sendcmd(f"RE{name.value}")
time.sleep(0.1)
return float(self.query("", size=-1))
def _register_write(self, name, value):
"""
Write a register on the HP3456a.
:param name: The name of the register to write to
:type name: `HP3456a.Register`
:type value: `float`
"""
try:
name = HP3456a.Register[name]
except KeyError:
pass
if not isinstance(name, HP3456a.Register):
raise TypeError(
"register must be specified as a "
"HP3456a.Register, got {} "
"instead.".format(name)
)
if name in [
HP3456a.Register.mean,
HP3456a.Register.variance,
HP3456a.Register.count,
]:
raise ValueError(f"register {name} is read only")
self.sendcmd(f"W{value}ST{name.value}")
time.sleep(0.1)
[docs]
def trigger(self):
"""
Signal a single manual trigger event to the `HP3456a`.
"""
self.sendcmd("T3")
# UNITS #######################################################################
UNITS = {
None: 1,
HP3456a.Mode.dcv: u.volt,
HP3456a.Mode.acv: u.volt,
HP3456a.Mode.acvdcv: u.volt,
HP3456a.Mode.resistance_2wire: u.ohm,
HP3456a.Mode.resistance_4wire: u.ohm,
HP3456a.Mode.ratio_dcv_dcv: 1,
HP3456a.Mode.ratio_acv_dcv: 1,
HP3456a.Mode.ratio_acvdcv_dcv: 1,
HP3456a.Mode.oc_resistence_2wire: u.ohm,
HP3456a.Mode.oc_resistence_4wire: u.ohm,
}