Rigol DP800-series supplies (DP821, DP831, DP832) are common on test benches because they are programmable, multi-channel, and inexpensive. This guide covers controlling one from Python over SCPI, sequencing the output safely, and recording what you measure as structured test data.
Prerequisites
- Python 3.10+
- A Rigol DP800-series supply on USB or Ethernet
pip install pyvisa pyvisa-py
Connecting
import pyvisarm = pyvisa.ResourceManager("@py")print(rm.list_resources())psu = rm.open_resource("TCPIP::192.168.1.101::INSTR")psu.timeout = 5000print(psu.query("*IDN?").strip())# RIGOL TECHNOLOGIES,DP832,DP8A1234567890,00.01.16If the supply is on USB and is not discovered on Linux, the usual cause is udev permissions rather than PyVISA.
The Commands You Actually Need
| Command | Purpose |
|---|---|
:INST:NSEL 1 | Select channel by number (1, 2, 3) |
:VOLT 5.0 | Set voltage on the selected channel |
:CURR 0.5 | Set current limit on the selected channel |
:OUTP CH1,ON | Enable a channel's output |
:OUTP CH1,OFF | Disable it |
:MEAS:VOLT? CH1 | Measure actual output voltage |
:MEAS:CURR? CH1 | Measure actual output current |
:MEAS:POWE? CH1 | Measure output power |
:OUTP:OVP:VAL CH1,6.0 | Over-voltage protection threshold |
:OUTP:OCP:VAL CH1,1.0 | Over-current protection threshold |
:OUTP:OVP CH1,ON | Enable OVP |
:SYST:ERR? | Read the error queue |
Rigol accepts both :INST:NSEL n followed by bare :VOLT, and the channel-qualified :APPL CH1,5.0,0.5 form. The channel-qualified form is harder to get wrong in a script, because there is no hidden selected-channel state.
A Minimal Session
rigol/basic.py26 lines
import pyvisaimport timerm = pyvisa.ResourceManager("@py")psu = rm.open_resource("TCPIP::192.168.1.101::INSTR")psu.timeout = 5000psu.write("*RST")psu.write("*CLS")# Configure channel 1 for 5 V with a 500 mA limitpsu.write(":APPL CH1,5.0,0.5")# Protection before the output is ever enabledpsu.write(":OUTP:OVP:VAL CH1,6.0")psu.write(":OUTP:OVP CH1,ON")psu.write(":OUTP CH1,ON")time.sleep(0.2) # Let the rail settle before measuringv = float(psu.query(":MEAS:VOLT? CH1"))i = float(psu.query(":MEAS:CURR? CH1"))print(f"{v:.3f} V, {i:.4f} A")psu.write(":OUTP CH1,OFF")psu.close()Two details that matter on a bench with a real board attached:
Set protection before enabling the output. OVP and OCP configured after the fact protect nothing during the inrush.
Measure after a settle delay. The supply will happily answer :MEAS:VOLT? mid-ramp and give you a number that is real but meaningless.
Wrapping It as a Plug
In a test, the supply belongs in a plug: opened once, shared by phases, and guaranteed to power down at the end even when a phase raises.
plugs/psu.py54 lines
import timeimport pyvisaclass PowerSupply: """Rigol DP800-series supply over SCPI.""" RESOURCE = "TCPIP::192.168.1.101::INSTR" SETTLE_S = 0.2 def __init__(self): rm = pyvisa.ResourceManager("@py") self.instr = rm.open_resource(self.RESOURCE) self.instr.timeout = 5000 self.instr.write("*RST") self.instr.write("*CLS") def configure(self, channel: int, volts: float, current_limit: float): self.instr.write(f":APPL CH{channel},{volts},{current_limit}") self.instr.write(f":OUTP:OVP:VAL CH{channel},{volts * 1.2:.3f}") self.instr.write(f":OUTP:OVP CH{channel},ON") def output(self, channel: int, on: bool): self.instr.write(f":OUTP CH{channel},{'ON' if on else 'OFF'}") if on: time.sleep(self.SETTLE_S) def measure_voltage(self, channel: int) -> float: return float(self.instr.query(f":MEAS:VOLT? CH{channel}")) def measure_current(self, channel: int) -> float: return float(self.instr.query(f":MEAS:CURR? CH{channel}")) def identify(self) -> str: return self.instr.query("*IDN?").strip() def errors(self) -> list[str]: out = [] while True: err = self.instr.query(":SYST:ERR?").strip() if err.startswith("0,"): break out.append(err) return out def close(self): """Always de-energise, even if a phase failed.""" for ch in (1, 2, 3): try: self.instr.write(f":OUTP CH{ch},OFF") except Exception: pass self.instr.close()close() is the important method. It runs at teardown whatever happened during the run, which is what stops a failed test leaving a board powered on the fixture overnight.
Using It in a Procedure
def power_up(measurements, psu): """Energise the board and confirm the rail came up under load.""" psu.configure(channel=1, volts=5.0, current_limit=0.5) psu.output(1, True) measurements.rail_5v = psu.measure_voltage(1) measurements.inrush_current = psu.measure_current(1)def idle_current(measurements, psu): """Board is booted; measure the quiescent draw.""" measurements.idle_current = psu.measure_current(1)def supply_health(measurements, psu): measurements.supply_errors = len(psu.errors())procedure.yaml51 lines
name: Board Power-Up Testversion: 1.0.0unit: serial_number: default_value: "PCBA000001" part_number: default_value: "PCBA-100"plugs: - name: psu python: plugs.psu:PowerSupplymain: - name: Power Up python: phases.power_up measurements: - name: Rail 5V unit: V validators: - operator: ">=" expected_value: 4.9 - operator: "<=" expected_value: 5.1 - name: Inrush Current unit: A validators: - operator: "<=" expected_value: 0.45 - name: Idle Current python: phases.idle_current depends_on: [power_up] measurements: - name: Idle Current unit: A validators: - operator: ">=" expected_value: 0.01 - operator: "<=" expected_value: 0.12 - name: Supply Health python: phases.supply_health depends_on: [idle_current] measurements: - name: Supply Errors description: SCPI error queue depth after the sequence validators: - operator: "==" expected_value: 0Run it with tofupilot run ./procedure.yaml.
The phases are chained with depends_on deliberately. They share one VISA session, and independent phases run concurrently, so a shared instrument must be serialised or given one session per phase.
The current-limit setting is worth a comment: at 0.5 A limit with an inrush limit of 0.45 A, a board that trips into constant-current mode will read a voltage well below 4.9 V and fail the rail measurement rather than silently passing at a collapsed rail.
Recording the Supply Identity
def record_equipment(run, psu): run.metadata["psu_id"] = psu.identify()Useful the first time two benches disagree and you need to know which supply produced which numbers.
Multi-Channel Boards
For a board with several rails, configure each channel and measure them together:
RAILS = {1: (5.0, 0.5), 2: (3.3, 0.5), 3: (1.8, 0.3)}def all_rails(measurements, psu): for ch, (volts, limit) in RAILS.items(): psu.configure(ch, volts, limit) psu.output(ch, True) measurements.rail_5v = psu.measure_voltage(1) measurements.rail_3v3 = psu.measure_voltage(2) measurements.rail_1v8 = psu.measure_voltage(3)Power-up order matters on many designs. If your board requires a sequence, enable the channels in that order with delays between them rather than all at once.
If You Already Run OpenHTF
The same supply wraps into a BasePlug:
rigol/openhtf_test.py39 lines
import openhtf as htffrom openhtf.plugs import BasePlugfrom openhtf.util import unitsfrom tofupilot.openhtf import uploadclass RigolPsu(BasePlug): def setUp(self): import pyvisa rm = pyvisa.ResourceManager("@py") self.instr = rm.open_resource("TCPIP::192.168.1.101::INSTR") self.instr.timeout = 5000 self.instr.write("*RST") def apply(self, channel: int, volts: float, limit: float): self.instr.write(f":APPL CH{channel},{volts},{limit}") self.instr.write(f":OUTP CH{channel},ON") def measure_voltage(self, channel: int) -> float: return float(self.instr.query(f":MEAS:VOLT? CH{channel}")) def tearDown(self): for ch in (1, 2, 3): self.instr.write(f":OUTP CH{ch},OFF") self.instr.close()@htf.measures( htf.Measurement("rail_5v").in_range(4.9, 5.1).with_units(units.VOLT),)@htf.plug(psu=RigolPsu)def test_power_up(test, psu): psu.apply(1, 5.0, 0.5) test.measurements.rail_5v = psu.measure_voltage(1)test = htf.Test(test_power_up, part_number="PCBA-100")test.add_output_callbacks(upload())test.execute(test_start=lambda: input("Scan serial: "))Install with pip install "tofupilot[openhtf]", or skip the callback and run it with tofupilot run.
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Voltage reads far below setpoint | Board is drawing the current limit | Raise the limit, or treat it as a genuine failure |
| Reading taken mid-ramp | No settle delay after :OUTP ON | Sleep before measuring |
-113, "Undefined header" | Command typo, or unsupported on this model | Check the model's programming guide |
| Output stays on after a failed run | Teardown did not execute | Disable outputs in the plug's close() |
| Intermittent timeouts under parallel phases | One VISA session used concurrently | Serialise with depends_on |
| USB device not found on Linux | udev permissions | Add a udev rule for the Rigol vendor ID |
