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 pyvisarm = pyvisa.ResourceManager("@py") # Backend Python purdmm = 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-rangevoltage = float(dmm.query(":MEAS:VOLT:DC?"))# Courant DC, auto-rangecurrent = float(dmm.query(":MEAS:CURR:DC?"))# Résistance, auto-rangeresistance = float(dmm.query(":MEAS:RES?"))# Tension ACac_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 Vdmm.write(":CONF:VOLT:DC 10")# Déclencher une mesuredmm.write(":INIT")# Attendre la findmm.query("*OPC?")# Lire le résultatvoltage = 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 pyvisaimport timerm = pyvisa.ResourceManager("@py")psu = rm.open_resource("TCPIP::192.168.1.101::INSTR")psu.timeout = 5000psu.write("*RST")psu.write("*CLS")# Sélectionner le canal et configurerpsu.write(":INST:SEL CH1")psu.write(":VOLT 5.0") # Définir la sortie à 5 Vpsu.write(":CURR 0.5") # Limite de courant à 500 mA# Activer la sortiepsu.write(":OUTP ON")time.sleep(0.5) # Attendre la stabilisation# Lire les valeurs réellesactual_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 finpsu.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 oscilloscopescope = rm.open_resource("TCPIP::192.168.1.102::INSTR")scope.timeout = 10000scope.write("*RST")scope.write(":CHAN1:DISP ON") # Activer le canal 1scope.write(":CHAN1:SCAL 1.0") # 1 V/divscope.write(":TIM:SCAL 0.001") # 1 ms/divscope.write(":TRIG:EDGE:SOUR CHAN1") # Déclencher sur le canal 1scope.write(":TRIG:EDGE:LEV 1.5") # Déclencher à 1,5 V# Mesurer la fréquence et l'amplitudefrequency = 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 commandeserrors = 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 calibredmm.write(":TRIG:SOUR BUS") # Mode déclenchement busdmm.write(":INIT") # Armer le système de déclenchement# ... autres configurations ...dmm.write("*TRG") # Envoyer le déclenchementdmm.query("*OPC?") # Attendre la finreading = 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 pyvisaimport openhtf as htffrom openhtf.plugs import BasePlugfrom openhtf.util import unitsfrom tofupilot.openhtf import TofuPilotclass 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 |