Instrument Control

Commandes SCPI en Python pour le test

Référence des commandes SCPI en Python avec PyVISA, couvrant les types de mesure, le déclenchement, la gestion des erreurs et l'intégration OpenHTF avec.

JJulien Buteau
beginner13 min de lecture14 mars 2026

SCPI (Standard Commands for Programmable Instruments) est le langage de commande que la plupart des instruments de test modernes utilisent. Si votre multimètre, alimentation ou oscilloscope a été fabriqué après 1990, il supporte presque certainement le SCPI. Ce guide couvre les commandes les plus utilisées en test de fabrication, comment les envoyer depuis Python avec PyVISA, et comment les intégrer dans des tests de production avec OpenHTF et TofuPilot.

Qu'est-ce que le SCPI

Le SCPI définit un ensemble standard de commandes textuelles pour contrôler les instruments. Au lieu d'apprendre un protocole différent pour chaque fabricant, vous utilisez la même structure de commandes pour Keysight, Rigol, Rohde & Schwarz, Tektronix et les autres.

Les commandes suivent une structure arborescente :

:MEASure :VOLTage :DC? → Mesurer la tension DC :AC? → Mesurer la tension AC :CURRent :DC? → Mesurer le courant DC :RESistance? → Mesurer la résistance

Le deux-points sépare les niveaux. Un point d'interrogation signifie « requête » (lire une valeur). Sans point d'interrogation, c'est une « commande » (définir quelque chose).

Installation

install.sh
pip install pyvisa pyvisa-py

Se connecter à un instrument :

scpi/connect.py
import pyvisa

rm = pyvisa.ResourceManager("@py")  # Backend Python pur
dmm = rm.open_resource("TCPIP::192.168.1.100::INSTR")
dmm.timeout = 5000  # 5 secondes

# Tout instrument SCPI répond à *IDN?
idn = dmm.query("*IDN?")
print(f"Connecté : {idn.strip()}")

Commandes SCPI courantes

Commandes obligatoires IEEE 488.2

Tout instrument SCPI les supporte :

CommandeFonctionRetour
*IDN?Identifier l'instrumentFabricant, modèle, série, firmware
*RSTRéinitialiser aux paramètres d'usineRien
*CLSEffacer le statut et la file d'erreursRien
*OPC?Requête de fin d'opération« 1 » quand terminé
*TST?Auto-test0 = réussi, autre = échec
*WAIAttendre les opérations en coursRien (bloque jusqu'à la fin)

Commencez toujours un test avec *RST et *CLS. Cela met l'instrument dans un état connu.

scpi/reset.py
dmm.write("*RST")
dmm.write("*CLS")

Commandes de mesure

Deux approches : raccourcie et explicite.

Raccourcie (configurer + déclencher + lire en une seule commande) :

scpi/measure_shorthand.py
# Tension DC, auto-range
voltage = float(dmm.query(":MEAS:VOLT:DC?"))

# Courant DC, auto-range
current = float(dmm.query(":MEAS:CURR:DC?"))

# Résistance, auto-range
resistance = float(dmm.query(":MEAS:RES?"))

# Tension AC
ac_voltage = float(dmm.query(":MEAS:VOLT:AC?"))

Explicite (configuration, déclenchement et lecture séparés pour plus de contrôle) :

scpi/measure_explicit.py
# Configurer pour la tension DC, calibre 10 V
dmm.write(":CONF:VOLT:DC 10")

# Déclencher une mesure
dmm.write(":INIT")

# Attendre la fin
dmm.query("*OPC?")

# Lire le résultat
voltage = float(dmm.query(":FETCH?"))

Utilisez l'approche explicite quand vous avez besoin d'un contrôle précis du timing ou quand vous mesurez plusieurs canaux en séquence.

ApprocheQuand l'utiliserCommandes
:MEAS:...?Lecture rapide unique1 commande
:CONF: + :INIT + :FETCH?Timing précis, multi-canaux3+ commandes
:CONF: + :READ?Configurer une fois, lire plusieurs fois2 commandes

Commandes d'alimentation

scpi/power_supply.py
import pyvisa
import time

rm = pyvisa.ResourceManager("@py")
psu = rm.open_resource("TCPIP::192.168.1.101::INSTR")
psu.timeout = 5000

psu.write("*RST")
psu.write("*CLS")

# Sélectionner le canal et configurer
psu.write(":INST:SEL CH1")
psu.write(":VOLT 5.0")          # Définir la sortie à 5 V
psu.write(":CURR 0.5")          # Limite de courant à 500 mA

# Activer la sortie
psu.write(":OUTP ON")
time.sleep(0.5)                  # Attendre la stabilisation

# Lire les valeurs réelles
actual_v = float(psu.query(":MEAS:VOLT?"))
actual_i = float(psu.query(":MEAS:CURR?"))
print(f"Sortie : {actual_v:.3f} V, {actual_i:.4f} A")

# Toujours désactiver la sortie à la fin
psu.write(":OUTP OFF")
psu.close()

Commandes d'alimentation courantes :

CommandeFonction
:INST:SEL CH1Sélectionner le canal de sortie
:VOLT 5.0Définir la tension
:CURR 0.5Définir la limite de courant
:OUTP ON / :OUTP OFFActiver/désactiver la sortie
:MEAS:VOLT?Lire la tension de sortie réelle
:MEAS:CURR?Lire le courant de sortie réel
:VOLT:PROT 6.0Définir la protection en surtension
:CURR:PROT 1.0Définir la protection en surcourant

Commandes d'oscilloscope

scpi/oscilloscope.py
# Configuration de base d'un oscilloscope
scope = rm.open_resource("TCPIP::192.168.1.102::INSTR")
scope.timeout = 10000

scope.write("*RST")
scope.write(":CHAN1:DISP ON")           # Activer le canal 1
scope.write(":CHAN1:SCAL 1.0")          # 1 V/div
scope.write(":TIM:SCAL 0.001")         # 1 ms/div
scope.write(":TRIG:EDGE:SOUR CHAN1")    # Déclencher sur le canal 1
scope.write(":TRIG:EDGE:LEV 1.5")      # Déclencher à 1,5 V

# Mesurer la fréquence et l'amplitude
frequency = float(scope.query(":MEAS:FREQ? CHAN1"))
amplitude = float(scope.query(":MEAS:VAMP? CHAN1"))

Gestion des erreurs

Les instruments signalent les erreurs via une file d'attente. Videz toujours la file après une séquence de commandes.

scpi/error_handling.py
def check_errors(instr) -> list[str]:
    """Lit toutes les erreurs de la file d'erreurs SCPI."""
    errors = []
    while True:
        err = instr.query(":SYST:ERR?").strip()
        code = int(err.split(",")[0])
        if code == 0:
            break
        errors.append(err)
    return errors


# Après une séquence de commandes
errors = check_errors(dmm)
if errors:
    for e in errors:
        print(f"  Erreur : {e}")
    raise RuntimeError(f"Erreurs instrument : {errors}")

Codes d'erreur SCPI courants :

CodeSignificationCause typique
-100Erreur de commandeFaute de frappe dans la commande
-200Erreur d'exécutionValeur de paramètre invalide
-300Erreur spécifique à l'appareilProblème matériel
-400Erreur de requêteRequête sans lecture de la réponse
0Pas d'erreurFile vide

Modèles de déclenchement

Le SCPI définit plusieurs sources de déclenchement. Le bon choix dépend de vos exigences de timing.

Source de déclenchementCommandeCas d'utilisation
Immédiat:TRIG:SOUR IMMPar défaut. Mesure dès la configuration.
Bus:TRIG:SOUR BUSDéclenchement logiciel avec *TRG. Timing précis.
Externe:TRIG:SOUR EXTLigne de déclenchement matérielle. Synchronisé avec d'autres instruments.
Timer:TRIG:SOUR TIMMesures périodiques à intervalles fixes.

Exemple de déclenchement bus (contrôle précis du timing) :

scpi/bus_trigger.py
dmm.write(":CONF:VOLT:DC 10")       # Configurer le calibre
dmm.write(":TRIG:SOUR BUS")         # Mode déclenchement bus
dmm.write(":INIT")                   # Armer le système de déclenchement
# ... autres configurations ...
dmm.write("*TRG")                   # Envoyer le déclenchement
dmm.query("*OPC?")                  # Attendre la fin
reading = float(dmm.query(":FETCH?"))

Différences entre fabricants

Le SCPI est un standard, mais les fabricants ajoutent leurs propres extensions. Différences courantes :

FonctionnalitéKeysightRigolRohde & Schwarz
Sélection du canal:INST:SEL CH1:INST CH1:INST:SEL 1
Capture d'écran:DISP:DATA? PNG:DISP:DATA?:HCOP:DATA?
Requête d'erreur:SYST:ERR?:SYST:ERR?:SYST:ERR:ALL?
Bip sonore:SYST:BEEP:SYST:BEEP:STAT ON:SYST:BEEP:IMM

Consultez toujours le manuel de programmation de votre instrument pour la syntaxe exacte. Les commandes de mesure de base (:MEAS:, :CONF:, :INIT, :FETCH?) sont cohérentes entre les fabricants.

Intégration avec OpenHTF et TofuPilot

Encapsulez les commandes SCPI dans un plug OpenHTF pour les tests de production. Le plug gère le cycle de vie de la connexion, et les mesures transitent automatiquement via OpenHTF vers TofuPilot.

scpi/openhtf_integration.py
import pyvisa
import openhtf as htf
from openhtf.plugs import BasePlug
from openhtf.util import units
from tofupilot.openhtf import TofuPilot


class ScpiDmm(BasePlug):
    """Plug multimètre SCPI avec gestion automatique du cycle de vie."""

    RESOURCE = "TCPIP::192.168.1.100::INSTR"

    def setUp(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 measure_dc_voltage(self, range_v: str = "AUTO") -> float:
        self.instr.write(f":CONF:VOLT:DC {range_v}")
        return float(self.instr.query(":MEAS:VOLT:DC?"))

    def measure_dc_current(self, range_a: str = "AUTO") -> float:
        self.instr.write(f":CONF:CURR:DC {range_a}")
        return float(self.instr.query(":MEAS:CURR:DC?"))

    def measure_resistance(self, range_ohm: str = "AUTO") -> float:
        self.instr.write(f":CONF:RES {range_ohm}")
        return float(self.instr.query(":MEAS:RES?"))

    def tearDown(self):
        self.instr.close()


@htf.measures(
    htf.Measurement("voltage_3v3")
    .in_range(3.2, 3.4)
    .with_units(units.VOLT),
)
@htf.plug(dmm=ScpiDmm)
def test_rail(test, dmm):
    test.measurements.voltage_3v3 = dmm.measure_dc_voltage()


def main():
    test = htf.Test(test_rail, procedure_id="FCT-001", part_number="PCBA-100")
    with TofuPilot(test):
        test.execute(test_start=lambda: input("Numéro de série : "))

Référence rapide

TâcheCommande SCPI
Identifier*IDN?
Réinitialiser*RST
Effacer les erreurs*CLS
Mesurer la tension DC:MEAS:VOLT:DC?
Mesurer le courant DC:MEAS:CURR:DC?
Mesurer la résistance:MEAS:RES?
Définir la tension (alim.):VOLT 5.0
Définir la limite de courant (alim.):CURR 0.5
Sortie on/off (alim.):OUTP ON / :OUTP OFF
Lire une erreur:SYST:ERR?
Attendre la fin*OPC?
Déclenchement logiciel*TRG

Plus de guides

Mettez ce guide en pratique