PyVISA
PyVISADocs

Multimeter Example

PyVISA guide for controlling digital multimeters - Keysight 34461A, automated measurements, data logging, and measurement techniques for voltage, current, and resistance.

Digital multimeter automation with PyVISA. From basic measurements to data logging and automated test sequences.

Quick Start Example

import pyvisa
import time

# Connect to Keysight 34461A DMM
rm = pyvisa.ResourceManager()
dmm = rm.open_resource("USB0::0x2A8D::0x0101::MY53220001::INSTR")

try:
    # Basic voltage measurement
    dmm.write("MEAS:VOLT:DC?")
    voltage = float(dmm.read())
    print(f"DC Voltage: {voltage:.6f} V")
    
finally:
    dmm.close()
    rm.close()

Supported Multimeters

This guide covers programming patterns that work with most SCPI-compatible DMMs:

Keysight (Agilent) Series

  • 34461A/34470A (6½ and 7½ digit) - Featured in examples
  • 34401A (6½ digit legacy)
  • U3606A (Handheld series)

Tektronix Series

  • DMM4050/4040 (6½ digit)
  • DMM4020 (5½ digit)

Fluke Series

  • 8845A/8846A (6½ digit)
  • 8808A (5½ digit)

Rohde & Schwarz

  • HMC8012 (5½ digit)

Complete Programming Guide

1. Connection Setup

import pyvisa
import time
import csv
from datetime import datetime

def connect_dmm(resource_string):
    """Connect to DMM with proper configuration"""
    rm = pyvisa.ResourceManager()
    
    try:
        dmm = rm.open_resource(resource_string)
        dmm.timeout = 10000  # 10 second timeout
        dmm.write_termination = '\n'
        dmm.read_termination = '\n'
        
        # Identify the instrument
        idn = dmm.query("*IDN?")
        print(f"Connected to: {idn}")
        
        # Reset to known state
        dmm.write("*RST")
        dmm.write("*CLS")  # Clear error queue
        
        return dmm, rm
        
    except pyvisa.VisaIOError as e:
        print(f"Connection failed: {e}")
        rm.close()
        raise

# Example usage
dmm, rm = connect_dmm("USB0::0x2A8D::0x0101::MY53220001::INSTR")

2. Basic Measurements

def basic_measurements(dmm):
    """Perform all basic measurement types"""
    
    measurements = {}
    
    # DC Voltage (default range auto)
    dmm.write("CONF:VOLT:DC")
    time.sleep(0.1)
    dmm.write("READ?")
    measurements['dc_voltage'] = float(dmm.read())
    
    # AC Voltage
    dmm.write("CONF:VOLT:AC")
    time.sleep(0.1)
    dmm.write("READ?")
    measurements['ac_voltage'] = float(dmm.read())
    
    # DC Current (be careful with current measurements!)
    dmm.write("CONF:CURR:DC")
    time.sleep(0.1)
    dmm.write("READ?")
    measurements['dc_current'] = float(dmm.read())
    
    # Resistance (2-wire)
    dmm.write("CONF:RES")
    time.sleep(0.1)
    dmm.write("READ?")
    measurements['resistance'] = float(dmm.read())
    
    # 4-wire resistance (more accurate for low resistance)
    dmm.write("CONF:FRES")
    time.sleep(0.1)
    dmm.write("READ?")
    measurements['resistance_4wire'] = float(dmm.read())
    
    return measurements

# Example usage
results = basic_measurements(dmm)
for measurement, value in results.items():
    print(f"{measurement}: {value}")

3. Advanced Measurement Configuration

ParameterDescriptionTypeDefaultValues
VOLT:DC:NPLCIntegration time in Power Line Cyclesnumber10.02, 0.2, 1, 10, 100
VOLT:DC:RANGEMeasurement range in voltsnumber | AUTOAUTO0.1, 1, 10, 100, 1000
VOLT:DC:ZERO:AUTOEnable/disable autozero for drift compensationON | OFFON-
VOLT:DC:RESMeasurement resolutionnumber | MAX | MINMedium-
VOLT:DC:IMPInput impedance in ohmsnumber10e910e6 (10 MOhm), 10e9 (10 GOhm)
def configure_advanced_measurement(dmm):
    """Configure DMM for high-precision measurements"""
    
    # Set integration time for better accuracy vs speed
    # Integration times: 0.02, 0.2, 1, 10, 100 PLCs (Power Line Cycles)
    dmm.write("VOLT:DC:NPLC 10")  # 10 PLC = best accuracy, slower
    
    # Set specific range (avoids range switching noise)
    dmm.write("VOLT:DC:RANGE 10")  # 10V range
    
    # Enable autozero for drift compensation
    dmm.write("VOLT:DC:ZERO:AUTO ON")
    
    # Set resolution (number of digits)
    dmm.write("VOLT:DC:RES MAX")  # Maximum resolution
    
    # Configure input impedance (for sensitive circuits)
    dmm.write("VOLT:DC:IMP:AUTO OFF")  # Disable auto impedance
    dmm.write("VOLT:DC:IMP 10e9")     # 10 GOhm fixed impedance
    
    print("Advanced measurement configuration complete")

def high_precision_voltage_measurement(dmm, samples=10):
    """Perform high-precision voltage measurement with statistics"""
    
    configure_advanced_measurement(dmm)
    
    measurements = []
    print(f"Taking {samples} high-precision measurements...")
    
    for i in range(samples):
        dmm.write("READ?")
        value = float(dmm.read())
        measurements.append(value)
        print(f"  Sample {i+1}: {value:.8f} V")
        time.sleep(0.5)  # Allow settling time
    
    # Calculate statistics
    import statistics
    mean_value = statistics.mean(measurements)
    std_dev = statistics.stdev(measurements) if len(measurements) > 1 else 0
    min_value = min(measurements)
    max_value = max(measurements)
    
    results = {
        'mean': mean_value,
        'std_dev': std_dev,
        'min': min_value,
        'max': max_value,
        'samples': measurements
    }
    
    print(f"\nMeasurement Statistics:")
    print(f"  Mean: {mean_value:.8f} V")
    print(f"  Std Dev: {std_dev:.8f} V")
    print(f"  Range: {min_value:.8f} to {max_value:.8f} V")
    
    return results

# Example usage
precision_results = high_precision_voltage_measurement(dmm, samples=20)

4. Data Logging and Continuous Monitoring

Data Logger Configuration

The DMM_DataLogger class provides professional data logging capabilities with automatic CSV export and statistics calculation.

class DMM_DataLogger:
    """Professional data logging class for DMM measurements"""
    
    def __init__(self, dmm, filename=None):
        self.dmm = dmm
        self.filename = filename or f"dmm_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
        self.measurements = []
        
    def configure_measurement(self, measurement_type='VOLT:DC', range_val='AUTO', 
                            nplc=1, samples=1):
        """Configure measurement parameters"""
        
        # Set measurement function
        self.dmm.write(f"CONF:{measurement_type}")
        
        # Set range
        if range_val != 'AUTO':
            self.dmm.write(f"{measurement_type}:RANGE {range_val}")
        
        # Set integration time
        self.dmm.write(f"{measurement_type}:NPLC {nplc}")
        
        # Configure for multiple samples
        self.dmm.write(f"SAMP:COUN {samples}")
        
        print(f"Configured for {measurement_type}, Range: {range_val}, NPLC: {nplc}")
    
    def log_single_measurement(self):
        """Log a single measurement with timestamp"""
        
        timestamp = datetime.now()
        self.dmm.write("READ?")
        value = float(self.dmm.read())
        
        measurement = {
            'timestamp': timestamp,
            'value': value,
            'iso_time': timestamp.isoformat()
        }
        
        self.measurements.append(measurement)
        return measurement
    
    def continuous_logging(self, duration_minutes=10, interval_seconds=1):
        """Continuously log measurements for specified duration"""
        
        end_time = datetime.now().timestamp() + (duration_minutes * 60)
        measurement_count = 0
        
        print(f"Starting continuous logging for {duration_minutes} minutes...")
        print("Press Ctrl+C to stop early")
        
        try:
            while datetime.now().timestamp() < end_time:
                measurement = self.log_single_measurement()
                measurement_count += 1
                
                print(f"#{measurement_count}: {measurement['value']:.6f} at {measurement['timestamp'].strftime('%H:%M:%S')}")
                
                time.sleep(interval_seconds)
                
        except KeyboardInterrupt:
            print(f"\nLogging stopped by user after {measurement_count} measurements")
        
        self.save_to_csv()
        return self.measurements
    
    def save_to_csv(self):
        """Save measurements to CSV file"""
        
        if not self.measurements:
            print("No measurements to save")
            return
        
        with open(self.filename, 'w', newline='') as csvfile:
            fieldnames = ['timestamp', 'iso_time', 'value']
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
            
            writer.writeheader()
            for measurement in self.measurements:
                writer.writerow(measurement)
        
        print(f"Saved {len(self.measurements)} measurements to {self.filename}")
    
    def get_statistics(self):
        """Calculate statistics from logged data"""
        
        if not self.measurements:
            return None
        
        values = [m['value'] for m in self.measurements]
        
        import statistics
        stats = {
            'count': len(values),
            'mean': statistics.mean(values),
            'median': statistics.median(values),
            'std_dev': statistics.stdev(values) if len(values) > 1 else 0,
            'min': min(values),
            'max': max(values),
            'range': max(values) - min(values)
        }
        
        return stats

# Example usage
logger = DMM_DataLogger(dmm, "battery_monitoring.csv")
logger.configure_measurement('VOLT:DC', range_val=10, nplc=1)

# Log for 5 minutes, one measurement per second
measurements = logger.continuous_logging(duration_minutes=5, interval_seconds=1)

# Get statistics
stats = logger.get_statistics()
print(f"\nLogging Statistics:")
for key, value in stats.items():
    if isinstance(value, float):
        print(f"  {key}: {value:.6f}")
    else:
        print(f"  {key}: {value}")

5. Battery Testing and Characterization

def battery_discharge_test(dmm, initial_voltage=12.0, cutoff_voltage=10.5):
    """Automated battery discharge test"""
    
    print("=== Battery Discharge Test ===")
    print(f"Monitoring voltage from {initial_voltage}V down to {cutoff_voltage}V")
    
    # Configure for battery voltage monitoring
    dmm.write("CONF:VOLT:DC")
    dmm.write("VOLT:DC:RANGE 100")  # Use 100V range for stability
    dmm.write("VOLT:DC:NPLC 1")    # 1 PLC for good balance of speed/accuracy
    
    start_time = datetime.now()
    measurements = []
    
    try:
        while True:
            # Take measurement
            dmm.write("READ?")
            voltage = float(dmm.read())
            current_time = datetime.now()
            elapsed_minutes = (current_time - start_time).total_seconds() / 60
            
            measurement = {
                'time_minutes': elapsed_minutes,
                'voltage': voltage,
                'timestamp': current_time
            }
            measurements.append(measurement)
            
            print(f"Time: {elapsed_minutes:6.1f} min, Voltage: {voltage:.3f} V")
            
            # Check cutoff condition
            if voltage <= cutoff_voltage:
                print(f"\nCutoff voltage {cutoff_voltage}V reached!")
                break
            
            # Check for unrealistic voltage (connection issues)
            if voltage < 0 or voltage > 50:
                print(f"Warning: Unusual voltage reading {voltage}V")
            
            time.sleep(60)  # Measure every minute
            
    except KeyboardInterrupt:
        print("\nTest stopped by user")
    
    # Save results
    filename = f"battery_test_{start_time.strftime('%Y%m%d_%H%M%S')}.csv"
    with open(filename, 'w', newline='') as csvfile:
        fieldnames = ['time_minutes', 'voltage', 'timestamp']
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(measurements)
    
    total_time = measurements[-1]['time_minutes'] if measurements else 0
    print(f"Test completed in {total_time:.1f} minutes")
    print(f"Results saved to {filename}")
    
    return measurements

# Example usage
# battery_test_results = battery_discharge_test(dmm)

6. Automated Component Testing

def resistor_tolerance_test(dmm, nominal_values, tolerance_percent=5):
    """Test multiple resistors for tolerance compliance"""
    
    print("=== Resistor Tolerance Test ===")
    
    # Configure for resistance measurement
    dmm.write("CONF:RES")
    dmm.write("RES:NPLC 10")  # High accuracy for component testing
    
    results = []
    
    for i, nominal in enumerate(nominal_values):
        input(f"\nConnect resistor #{i+1} (nominal {nominal} Ω) and press Enter...")
        
        # Take multiple measurements for accuracy
        measurements = []
        for j in range(5):
            dmm.write("READ?")
            value = float(dmm.read())
            measurements.append(value)
            time.sleep(0.2)
        
        # Calculate average
        avg_resistance = sum(measurements) / len(measurements)
        deviation_percent = ((avg_resistance - nominal) / nominal) * 100
        within_tolerance = abs(deviation_percent) <= tolerance_percent
        
        result = {
            'resistor_number': i + 1,
            'nominal_ohms': nominal,
            'measured_ohms': avg_resistance,
            'deviation_percent': deviation_percent,
            'within_tolerance': within_tolerance,
            'tolerance_limit': tolerance_percent
        }
        
        results.append(result)
        
        # Print result
        status = "PASS" if within_tolerance else "FAIL"
        print(f"Resistor #{i+1}: {avg_resistance:.2f} Ω ({deviation_percent:+.2f}%) - {status}")
    
    # Summary
    passed = sum(1 for r in results if r['within_tolerance'])
    print(f"\nTest Summary: {passed}/{len(results)} resistors passed tolerance test")
    
    return results

# Example usage
# Test 1kΩ, 10kΩ, 100kΩ resistors with 5% tolerance
# test_results = resistor_tolerance_test(dmm, [1000, 10000, 100000], tolerance_percent=5)

7. Error Handling and Robustness

def robust_dmm_operation(dmm, operation_func, max_retries=3):
    """Wrapper for robust DMM operations with error handling"""
    
    for attempt in range(max_retries):
        try:
            # Clear any previous errors
            dmm.write("*CLS")
            
            # Perform operation
            result = operation_func(dmm)
            
            # Check for instrument errors
            error = dmm.query("SYST:ERR?")
            if not error.startswith("+0,"):
                print(f"Instrument error: {error}")
                if attempt < max_retries - 1:
                    time.sleep(1)
                    continue
                else:
                    raise Exception(f"Instrument error after {max_retries} attempts: {error}")
            
            return result
            
        except pyvisa.VisaIOError as e:
            print(f"VISA error (attempt {attempt + 1}): {e}")
            if attempt < max_retries - 1:
                time.sleep(2)  # Wait before retry
                continue
            else:
                raise
        except Exception as e:
            print(f"Unexpected error (attempt {attempt + 1}): {e}")
            if attempt < max_retries - 1:
                time.sleep(1)
                continue
            else:
                raise
    
    return None

def safe_voltage_measurement(dmm):
    """Safe voltage measurement function"""
    dmm.write("CONF:VOLT:DC")
    dmm.write("READ?")
    return float(dmm.read())

# Example usage with error handling
try:
    voltage = robust_dmm_operation(dmm, safe_voltage_measurement)
    print(f"Measured voltage: {voltage:.6f} V")
except Exception as e:
    print(f"Measurement failed: {e}")

Performance Optimization

Fast Measurement Techniques

def fast_measurement_burst(dmm, count=100):
    """Optimized for maximum measurement speed"""
    
    # Configure for speed
    dmm.write("CONF:VOLT:DC")
    dmm.write("VOLT:DC:NPLC 0.02")  # Minimum integration time
    dmm.write("VOLT:DC:ZERO:AUTO OFF")  # Disable autozero for speed
    dmm.write(f"SAMP:COUN {count}")
    
    # Trigger burst measurement
    start_time = time.time()
    dmm.write("READ?")
    
    # Read all measurements at once
    response = dmm.read()
    end_time = time.time()
    
    # Parse comma-separated values
    values = [float(x) for x in response.split(',')]
    
    measurement_rate = len(values) / (end_time - start_time)
    print(f"Completed {len(values)} measurements in {end_time - start_time:.3f}s")
    print(f"Measurement rate: {measurement_rate:.1f} measurements/second")
    
    return values

# Example usage
# fast_data = fast_measurement_burst(dmm, count=50)

Best Practices Summary

Do This:

  • Always use try/finally for resource cleanup
  • Set appropriate timeout values for your application
  • Use context managers when possible
  • Check instrument errors regularly with SYST:ERR?
  • Configure measurement parameters explicitly
  • Use appropriate integration times (NPLC) for your accuracy needs

Avoid This:

  • Leaving connections open indefinitely
  • Using default timeouts for critical measurements
  • Ignoring instrument error messages
  • Rapidly switching measurement functions without delays
  • Using auto-ranging when you know the expected range

Troubleshooting Tips:

  • If measurements seem unstable, increase NPLC (integration time)
  • For battery-powered devices, consider input impedance settings
  • Use 4-wire resistance for low resistance measurements
  • Check cable connections for intermittent readings
  • Monitor instrument temperature for precision work

Next Steps

How is this guide?