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
pip install pyvisa pyvisa-pySe connecter à un instrument :
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 :
| Commande | Fonction | Retour |
|---|---|---|
*IDN? | Identifier l'instrument | Fabricant, modèle, série, firmware |
*RST | Réinitialiser aux paramètres d'usine | Rien |
*CLS | Effacer le statut et la file d'erreurs | Rien |
*OPC? | Requête de fin d'opération | « 1 » quand terminé |
*TST? | Auto-test | 0 = réussi, autre = échec |
*WAI | Attendre les opérations en cours | Rien (bloque jusqu'à la fin) |
Commencez toujours un test avec *RST et *CLS. Cela met l'instrument dans un état connu.
dmm.write("*RST")
dmm.write("*CLS")Commandes de mesure
Deux approches : raccourcie et explicite.
Raccourcie (configurer + déclencher + lire en une seule commande) :
# 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) :
# 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.
| Approche | Quand l'utiliser | Commandes |
|---|---|---|
:MEAS:...? | Lecture rapide unique | 1 commande |
:CONF: + :INIT + :FETCH? | Timing précis, multi-canaux | 3+ commandes |
:CONF: + :READ? | Configurer une fois, lire plusieurs fois | 2 commandes |
Commandes d'alimentation
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 :
| Commande | Fonction |
|---|---|
:INST:SEL CH1 | Sélectionner le canal de sortie |
:VOLT 5.0 | Définir la tension |
:CURR 0.5 | Définir la limite de courant |
:OUTP ON / :OUTP OFF | Activer/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.0 | Définir la protection en surtension |
:CURR:PROT 1.0 | Définir la protection en surcourant |
Commandes d'oscilloscope
# 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.
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 :
| Code | Signification | Cause typique |
|---|---|---|
| -100 | Erreur de commande | Faute de frappe dans la commande |
| -200 | Erreur d'exécution | Valeur de paramètre invalide |
| -300 | Erreur spécifique à l'appareil | Problème matériel |
| -400 | Erreur de requête | Requête sans lecture de la réponse |
| 0 | Pas d'erreur | File 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éclenchement | Commande | Cas d'utilisation |
|---|---|---|
| Immédiat | :TRIG:SOUR IMM | Par défaut. Mesure dès la configuration. |
| Bus | :TRIG:SOUR BUS | Déclenchement logiciel avec *TRG. Timing précis. |
| Externe | :TRIG:SOUR EXT | Ligne de déclenchement matérielle. Synchronisé avec d'autres instruments. |
| Timer | :TRIG:SOUR TIM | Mesures périodiques à intervalles fixes. |
Exemple de déclenchement bus (contrôle précis du timing) :
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é | Keysight | Rigol | Rohde & 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.
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âche | Commande 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 |