Source code for pslab.instrument.multimeter

"""The PSLab's multimeter can measure voltage, resistance, and capacitance."""
import time
from typing import Tuple

import numpy as np
from scipy.optimize import curve_fit

import pslab.protocol as CP
from pslab.instrument.analog import GAIN_VALUES, INPUT_RANGES
from pslab.instrument.oscilloscope import Oscilloscope
from pslab.serial_handler import SerialHandler

_MICROSECONDS = 1e-6


[docs]class Multimeter(Oscilloscope): """Measure voltage, resistance and capacitance. Parameters ---------- device : Handler Serial interface for communicating with the PSLab device. If not provided, a new one will be created. """ _CURRENTS = [5.5e-4, 5.5e-7, 5.5e-6, 5.5e-5] _CURRENTS_RANGES = [1, 2, 3, 0] # Smallest first, _RC_RESISTANCE = 1e4 _CAPACITOR_CHARGED_VOLTAGE = 0.9 * max(INPUT_RANGES["CAP"]) _CAPACITOR_DISCHARGED_VOLTAGE = 0.01 * max(INPUT_RANGES["CAP"]) def __init__(self, device: SerialHandler = None): self._stray_capacitance = 5e-11 super().__init__(device)
[docs] def measure_resistance(self) -> float: """Measure the resistance of a resistor connected between RES and GND. Returns ------- resistance : float Resistance in ohm. """ voltage = self.measure_voltage("RES") resolution = max(INPUT_RANGES["RES"]) / ( 2 ** self._channels["RES"].resolution - 1 ) if voltage >= max(INPUT_RANGES["RES"]) - resolution: return np.inf pull_up_resistance = 5.1e3 current = (INPUT_RANGES["RES"][1] - voltage) / pull_up_resistance return voltage / current
[docs] def measure_voltage(self, channel: str = "VOL") -> float: """Measure the voltage on the selected channel. Parameters ---------- channel : {"CH1", "CH2", "CH3", "MIC", "CAP", "RES", "VOL"}, optional The name of the analog input on which to measure the voltage. The default channel is VOL. Returns ------- voltage : float Voltage in volts. """ self._voltmeter_autorange(channel) return self._measure_voltage(channel)
def _measure_voltage(self, channel: str) -> float: self._channels[channel].resolution = 12 scale = self._channels[channel].scale chosa = self._channels[channel].chosa self._device.send_byte(CP.ADC) self._device.send_byte(CP.GET_VOLTAGE_SUMMED) self._device.send_byte(chosa) raw_voltage_sum = self._device.get_int() # Sum of 16 samples. self._device.get_ack() raw_voltage_mean = round(raw_voltage_sum / 16) voltage = scale(raw_voltage_mean) return voltage def _voltmeter_autorange(self, channel: str) -> float: if channel in ("CH1", "CH2"): self._set_gain(channel, 1) # Reset gain. voltage = self._measure_voltage(channel) for gain in GAIN_VALUES[::-1]: rng = max(INPUT_RANGES[channel]) / gain if abs(voltage) < rng: break self._set_gain(channel, gain) return rng else: return max(INPUT_RANGES[channel])
[docs] def calibrate_capacitance(self): """Calibrate stray capacitance. Correctly calibrated stray capacitance is important when measuring small capacitors (picofarad range). Stray capacitace should be recalibrated if external wiring is connected to the CAP pin. """ for charge_time in np.unique(np.int16(np.logspace(2, 3))): self._discharge_capacitor() voltage, capacitance = self._measure_capacitance(1, 0, charge_time) if voltage >= self._CAPACITOR_CHARGED_VOLTAGE: break self._stray_capacitance += capacitance
[docs] def measure_capacitance(self) -> float: """Measure the capacitance of a capacitor connected between CAP and GND. Returns ------- capacitance : float Capacitance in Farad. """ for current_range in self._CURRENTS_RANGES: for i, charge_time in enumerate([50000, 5000, 500, 50, 5]): voltage, _ = self._measure_capacitance(current_range, 0, charge_time) if voltage < self._CAPACITOR_CHARGED_VOLTAGE: if i: return self._binary_search_capacitance( current_range, charge_time, charge_time * 10 ) else: break # Increase current. # Capacitor too big, use alternative method. return self._measure_rc_capacitance()
def _binary_search_capacitance( self, current_range: int, low_charge_time: int, high_charge_time: int, ) -> float: charge_time = (high_charge_time + low_charge_time) // 2 voltage, capacitance = self._measure_capacitance( current_range, 0, charge_time, ) if voltage / self._CAPACITOR_CHARGED_VOLTAGE < 0.98: return self._binary_search_capacitance( current_range, charge_time, high_charge_time, ) elif voltage / self._CAPACITOR_CHARGED_VOLTAGE > 1.02: return self._binary_search_capacitance( current_range, low_charge_time, charge_time, ) else: return capacitance def _set_cap(self, state, charge_time): """Set CAP HIGH or LOW.""" self._device.send_byte(CP.ADC) self._device.send_byte(CP.SET_CAP) self._device.send_byte(state) self._device.send_int(charge_time) self._device.get_ack() def _discharge_capacitor( self, discharge_time: int = 50000, timeout: float = 1 ) -> float: start_time = time.time() voltage = previous_voltage = self.measure_voltage("CAP") while voltage > self._CAPACITOR_DISCHARGED_VOLTAGE: self._set_cap(0, discharge_time) voltage = self.measure_voltage("CAP") if abs(previous_voltage - voltage) < self._CAPACITOR_DISCHARGED_VOLTAGE: break previous_voltage = voltage if time.time() - start_time > timeout: break return voltage def _measure_capacitance( self, current_range: int, trim: int, charge_time: int ) -> Tuple[float, float]: self._discharge_capacitor() self._channels["CAP"].resolution = 12 self._device.send_byte(CP.COMMON) self._device.send_byte(CP.GET_CAPACITANCE) self._device.send_byte(current_range) if trim < 0: self._device.send_byte(int(31 - abs(trim) / 2) | 32) else: self._device.send_byte(int(trim / 2)) self._device.send_int(charge_time) time.sleep(charge_time * _MICROSECONDS) raw_voltage = self._device.get_int() voltage = self._channels["CAP"].scale(raw_voltage) self._device.get_ack() charge_current = self._CURRENTS[current_range] * (100 + trim) / 100 if voltage: capacitance = ( charge_current * charge_time * _MICROSECONDS / voltage - self._stray_capacitance ) else: capacitance = 0 return voltage, capacitance def _measure_rc_capacitance(self) -> float: """Measure the capacitance by discharge through a 10K resistor.""" (x,) = self.capture("CAP", CP.MAX_SAMPLES, 10, block=False) x *= _MICROSECONDS self._set_cap(1, 50000) # charge self._set_cap(0, 50000) # discharge (y,) = self.fetch_data() if y.max() >= self._CAPACITOR_CHARGED_VOLTAGE: discharge_start = np.where(y >= self._CAPACITOR_CHARGED_VOLTAGE)[0][-1] else: discharge_start = np.where(y == y.max())[0][-1] x = x[discharge_start:] y = y[discharge_start:] # CAP floats for a brief period of time (~500 µs) between being set # HIGH until it is set LOW. This data is not useful and should be # discarded. When CAP is set LOW the voltage declines sharply, which # manifests as a negative peak in the time derivative. dydx = np.diff(y) / np.diff(x) cap_low = np.where(dydx == dydx.min())[0][0] x = x[cap_low:] y = y[cap_low:] # Discard data after the voltage reaches zero (improves fit). try: v_zero = np.where(y == 0)[0][0] x = x[:v_zero] y = y[:v_zero] except IndexError: pass # Remove time offset. x -= x[0] def discharging_capacitor_voltage( x: np.ndarray, v_init: float, rc_time_constant: float ) -> np.ndarray: return v_init * np.exp(-x / rc_time_constant) # Solve discharging_capacitor_voltage for rc_time_constant. rc_time_constant_guess = (-x[1:] / np.log(y[1:] / y[0])).mean() guess = [y[0], rc_time_constant_guess] popt, _ = curve_fit(discharging_capacitor_voltage, x, y, guess) rc_time_constant = popt[1] rc_capacitance = rc_time_constant / self._RC_RESISTANCE return rc_capacitance