Driving a 6.5-digit bench multimeter from Python: the commands that matter, how NPLC trades speed against accuracy, buffered acquisition for fast logging, and a complete logger.

A bench multimeter is automated from Python with PyVISA: configure the function once, then trigger and read in a loop. The two things that separate a working logger from a fast, accurate one are understanding NPLC and using the instrument's buffer rather than querying point by point.
This guide covers connection, every common measurement function, the accuracy and speed trade, buffered acquisition, and a complete logging script.
import pyvisa
rm = pyvisa.ResourceManager()
dmm = rm.open_resource("USB0::0x2A8D::0x1301::MY57200001::INSTR")
dmm.timeout = 10000
print(dmm.query("*IDN?").strip())
dmm.write("*RST")
dmm.write("*CLS")*RST matters more on a DMM than on most instruments, because leftover configuration such as a manual range or a disabled autozero silently changes your results. See the PyVISA tutorial for backends and resource strings.
| Measurement | Command |
|---|---|
| DC voltage | CONF:VOLT:DC <range>,<resolution> |
| AC voltage | CONF:VOLT:AC <range>,<resolution> |
| DC current | CONF:CURR:DC <range>,<resolution> |
| AC current | CONF:CURR:AC <range>,<resolution> |
| 2-wire resistance | CONF:RES <range>,<resolution> |
| 4-wire resistance | CONF:FRES <range>,<resolution> |
| Frequency | CONF:FREQ |
| Capacitance | CONF:CAP |
| Temperature (type K) | CONF:TEMP TC,K |
| Diode | CONF:DIOD |
| Continuity | CONF:CONT |
Then READ? to trigger and return.
dmm.write("CONF:VOLT:DC 10,0.00001") # 10 V range, 10 uV resolution
voltage = float(dmm.query("READ?"))Set the range explicitly rather than relying on autorange in a loop. Autorange adds a range-hunting delay to every reading and can produce a discontinuity in logged data when it switches.
The single most useful setting on a bench DMM and the one most often left at default.
dmm.write("VOLT:DC:NPLC 1")| NPLC | Time per reading (50 Hz) | Character |
|---|---|---|
| 0.02 | ~0.4 ms | Fast, noisy, no mains rejection |
| 0.2 | ~4 ms | Compromise |
| 1 | ~20 ms | Good mains rejection, sensible default |
| 10 | ~200 ms | Quiet, high resolution |
| 100 | ~2 s | Metrology, rarely needed |
NPLC 1 or higher rejects mains hum, because integrating over a whole number of line cycles averages the interference to zero. Below 1 you lose that, which is why fast logging is noisier in a way that no amount of averaging afterwards fixes.
Choose NPLC from the measurement, not from habit. A thermal soak logging at 1 Hz should use NPLC 10. A transient capture at 1 kHz has no choice but 0.02.
Querying READ? in a Python loop caps out around 30 to 100 readings per second because each one is a round trip. For anything faster, let the instrument fill its buffer and read it in one transfer.
dmm.write("CONF:VOLT:DC 10,0.0001")
dmm.write("VOLT:DC:NPLC 0.02")
dmm.write("VOLT:DC:RANG:AUTO OFF")
dmm.write("VOLT:DC:ZERO:AUTO OFF") # autozero doubles the time per reading
dmm.write("TRIG:SOUR IMM")
dmm.write("TRIG:COUN 1")
dmm.write("SAMP:COUN 10000")
dmm.write("INIT") # start filling the buffer
dmm.query("*OPC?") # block until complete
raw = dmm.query("FETC?") # one transfer for all 10000
values = [float(v) for v in raw.strip().split(",")]
print(f"{len(values)} readings, mean {sum(values)/len(values):.6f} V")Four settings do the work here:
Disabling autozero trades drift for speed. For a run under a minute this is fine. For a long soak, leave autozero on and accept the rate.
For very large buffers use binary transfer instead of ASCII:
dmm.write("FORM:DATA REAL,64")
values = dmm.query_binary_values("FETC?", datatype="d")Below roughly 100 ohms, lead resistance is a significant fraction of what you are measuring.
dmm.write("CONF:FRES 100,0.001")
dmm.write("FRES:NPLC 10")
resistance = float(dmm.query("READ?"))Connect both pairs at the device: the source pair carries the test current, the sense pair measures the voltage across the device only. Two-wire on a 50 milliohm shunt with 100 milliohms of leads reads three times the true value.
dmm.write("CONF:TEMP TC,K")
dmm.write("TEMP:TRAN:TC:RJUN:TYPE INT") # internal cold junction
temperature = float(dmm.query("READ?"))The cold junction reference is the accuracy limit. An internal reference is convenient and typically good to about a degree. For better, use an external ice-point or a fixed reference block.
import csv
import time
import pyvisa
DMM_ADDR = "USB0::0x2A8D::0x1301::MY57200001::INSTR"
DURATION_S = 3600
INTERVAL_S = 1.0
LIMITS = (3.2, 3.4)
rm = pyvisa.ResourceManager()
dmm = rm.open_resource(DMM_ADDR)
dmm.timeout = 15000
rows = []
try:
print(dmm.query("*IDN?").strip())
dmm.write("*RST"); dmm.write("*CLS")
dmm.write("CONF:VOLT:DC 10,0.00001")
dmm.write("VOLT:DC:NPLC 10") # quiet, slow logging
dmm.write("TRIG:SOUR IMM")
error = dmm.query("SYST:ERR?").strip()
if not error.startswith(("0,", "+0,")):
raise RuntimeError(f"configuration rejected: {error}")
start = time.time()
next_sample = start
while time.time() - start < DURATION_S:
next_sample += INTERVAL_S
value = float(dmm.query("READ?"))
elapsed = time.time() - start
verdict = "PASS" if LIMITS[0] <= value <= LIMITS[1] else "FAIL"
rows.append({"t_s": round(elapsed, 3), "volts": value, "verdict": verdict})
if verdict == "FAIL":
print(f"{elapsed:8.1f} s {value:.6f} V OUT OF LIMITS")
sleep_for = next_sample - time.time()
if sleep_for > 0:
time.sleep(sleep_for)
finally:
dmm.close(); rm.close()
with open("log.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["t_s", "volts", "verdict"])
writer.writeheader(); writer.writerows(rows)
fails = [r for r in rows if r["verdict"] == "FAIL"]
print(f"{len(rows)} samples, {len(fails)} out of limits")Note the timing approach. next_sample += INTERVAL_S schedules against a fixed grid rather than sleeping a fixed amount after each reading. The naive version accumulates the measurement time into the interval, so a "1 Hz" log at NPLC 10 actually runs at about 0.83 Hz and drifts further the longer it runs.
Before trusting a number to six digits:
The measurements above are self-triggered, which is fine when nothing else is moving. When a supply is stepping or a generator is firing, you need the DMM to measure at a known moment.
External trigger, where another instrument tells the DMM when to measure:
dmm.write("TRIG:SOUR EXT")
dmm.write("TRIG:SLOP NEG")
dmm.write("SAMP:COUN 1")
dmm.write("INIT") # armed, waiting for the trigger edge
# ... the generator or supply fires, the DMM measures ...
value = float(dmm.query("FETC?"))Software trigger, where your script decides:
dmm.write("TRIG:SOUR BUS")
dmm.write("INIT")
psu.write("VOLT 3.6") # make the change
time.sleep(SETTLE_S)
dmm.write("*TRG") # measure now
value = float(dmm.query("FETC?"))The INIT then FETC? pattern is the important part. INIT arms the instrument and returns immediately, so the DMM is already waiting when the event happens. Using READ? instead means the DMM only starts measuring after your script asks, which adds a round trip of uncertainty to the timing.
Trigger delay lets the instrument handle settling rather than your script:
dmm.write("TRIG:DEL 0.05") # 50 ms after the trigger, then measureThis is more repeatable than time.sleep() in Python, because it is timed by the instrument rather than by an operating system that may be doing something else.
For long runs where you only need the summary, the instrument can compute it and save the transfer entirely.
dmm.write("CALC:AVER:STAT ON")
dmm.write("SAMP:COUN 10000")
dmm.write("INIT")
dmm.query("*OPC?")
average = float(dmm.query("CALC:AVER:AVER?"))
minimum = float(dmm.query("CALC:AVER:MIN?"))
maximum = float(dmm.query("CALC:AVER:MAX?"))
stdev = float(dmm.query("CALC:AVER:SDEV?"))
count = int(float(dmm.query("CALC:AVER:COUN?")))
print(f"{count} samples: mean {average:.6f} V, "
f"range {minimum:.6f} to {maximum:.6f}, sd {stdev:.6f}")Five queries instead of a 10,000-point transfer. Use this for pass and fail checks on stability, and transfer the full record only when you need to see the shape of what happened.
Autorange is convenient and it is the wrong default for a logging script. Two reasons, and both bite quietly.
The first is speed. An autoranging DMM re-hunts the range on every reading when the signal sits near a boundary, which adds tens of milliseconds per point and makes your sample interval irregular. The second is worse: a range change mid-log produces a step in the recorded data that looks like a real event in the DUT.
dmm.write("CONF:VOLT:DC 10,0.0001") # fixed 10 V range, 100 uV resolution
dmm.write("VOLT:DC:RANG 10") # explicit, belt and braces
dmm.write("VOLT:DC:RANG:AUTO OFF")Pick the range from the largest value the test can legitimately produce, not the nominal one. A 3.3 V rail that can overshoot to 3.9 V during a transient belongs on the 10 V range, not on whatever autorange chose while the DUT was idle.
The accuracy consequence is worth knowing precisely: DMM accuracy is specified as a percentage of reading plus a percentage of range. On the 10 V range, the range term is fixed regardless of whether you are measuring 3.3 V or 9 V, which is why measuring a 3.3 V rail on the 1000 V range is so much worse than it looks. See read a digital multimeter datasheet for how to turn those two terms into an actual uncertainty number.
The NPLC section above explains the dial. This is the table for picking a value without experimenting.
| NPLC | Reading rate (60 Hz mains) | Use it for |
|---|---|---|
| 0.02 | ~300/s | Fast scans where you only need 4.5 digits |
| 0.2 | ~50/s | General logging, the sensible default |
| 1 | ~10/s | Good mains rejection, most characterisation work |
| 10 | ~1/s | Low-level DC, thermocouples, high-accuracy points |
| 100 | ~0.1/s | Calibration-grade measurements only |
NPLC of 1 or above gives you integer-cycle integration, which rejects mains hum. Below 1 you lose that, which is why a 0.02 NPLC log of a millivolt signal looks noisy and the same signal at NPLC 10 does not. If your readings wobble by a few counts and the wobble goes away when you raise NPLC, it was mains, not the DUT.
CONF:, then loop READ? or use the buffer.SYST:LFR 50 or 60. Wrong value means NPLC integration no longer aligns with the mains cycle and rejection disappears.fh.flush() per row on long runs.Logging a voltage is easy. Logging it while stepping a supply, checking limits, and producing a signed report is the actual job, and that is the part worth generating.
Connect your instruments. Pick the manufacturer and model, paste the VISA address (USB, LAN, GPIB, or serial), and the agent knows what is on your bench. No bench yet? Use a placeholder address, build the full automation, and swap in the real address when you are in the lab.
Tell the agent what to test, in plain English. For example, "run a VI sweep from 1 to 10 V in 1 V steps at 0.5 A load current," or "suggest the tests for a power-management device."
The agent builds the complete workflow in seconds. Instrument-aware automation appears on the canvas, with the generated scripts visible in a code panel you can inspect and edit.
Run it in your lab. Click Run and the status panel streams results step by step, with measured values inline (VOUT = 3.301 V, asserted 3.2 to 3.4 V, PASS). One click exports a structured PDF report, or the raw results as CSV.


The step-by-step walkthrough, VISA address formats, and Test Planner prompts are all in the TestFlow product guide.
Open the instrument with PyVISA, configure the function with CONF:VOLT:DC, then query READ? which triggers a measurement and returns it as a string you convert to float.
Number of power line cycles, the integration time for each reading. Higher NPLC means a longer measurement, less noise, and better mains hum rejection. NPLC 1 is a good default, NPLC 10 for quiet high-resolution work, NPLC 0.02 for speed.
MEAS configures, triggers, and returns in one command, which is simple but slow in a loop. READ triggers and returns using the existing configuration. FETCh returns the last result without triggering. Use CONF once then READ in a loop.
Lower NPLC, disable autorange and autozero, configure a sample count, and read the whole buffer at once with INIT then FETCh rather than querying point by point. Round-trip latency dominates at high rates.
Use CONF:FRES instead of CONF:RES, and connect both the source and sense terminal pairs to the device. Four-wire removes lead resistance from the reading, which matters below roughly 100 ohms.
Usually thermal EMF at connections, autozero disabled, or insufficient warm-up. Bench DMMs need 30 to 60 minutes to reach specified accuracy. For low-level DC, also check for dissimilar metal junctions in the path.
Connect your instruments, describe a test in plain English, and TestFlow builds and runs it in minutes.
A new way for testing, from specs to automated sequences, capture clean data, and accelerate your validation cycle.