Instrument Control

Partager des instruments entre slots

Les instruments partagés entrelacent les commandes et détruisent le débit. Comment la portée des plugs sérialise les appels et dimensionner le goulot.

JJulien Buteau
advanced10 min de lecture9 septembre 2026

Partager des instruments entre slots parallèles

Le test parallèle suppose que chaque position a ses propres instruments. Les postes réels le font rarement. Un multimètre derrière un multiplexeur sert huit positions, une enceinte thermique contient tout le plateau, une alimentation nourrit un rack entier.

Les instruments partagés sont l'endroit où les postes de test parallèles perdent le débit pour lequel ils ont été construits, et où ils produisent des mesures fausses d'une manière qui passe inaperçue. Ce guide couvre le fonctionnement du partage, comment en dimensionner le bénéfice, et les modes de défaillance contre lesquels il faut concevoir.

Il suppose un banc multi-positions ; pour savoir ce qu'est une position et comment la déclarer, voir Que sont les test sockets.

Pourquoi le partage pose problème

Un instrument est un matériel unique doté d'un état. Configurez un multimètre sur un calibre 10 V continu, et ce sera son calibre jusqu'à ce que quelque chose le change.

Deux slots mesurant en même temps créent deux défaillances qui ne se ressemblent pas :

Commandes entrelacées. Le slot 1 règle le calibre sur 10 V, le slot 2 le règle sur 100 mV, puis le slot 1 déclenche une lecture. Le slot 1 mesure son unité sur le calibre du slot 2. L'instrument ne signale aucune erreur, la mesure est un nombre plausible, et l'unité soit échoue sans raison, soit passe alors qu'elle ne devrait pas.

Sérialisation. Deux slots ne peuvent pas physiquement lire un multimètre simultanément. Les appels sont mis en file, que vous l'ayez conçu ainsi ou non.

La première défaillance est la dangereuse, parce qu'elle produit de mauvaises données plutôt qu'une erreur.

La portée décide du nombre d'instances

Dans le TofuPilot Framework, la portée du plug contrôle le nombre d'instances et leur durée de vie :

PortéeInstanceÀ utiliser pour
slot (défaut)Une par slotVoie d'alimentation par position, port série par position
executionUne partagée par tous les slotsUn multimètre derrière un multiplexeur, une enceinte, une alimentation de rack
stationUne par poste, conservée entre exécutionsSessions VISA ou TCP lentes à ouvrir
procedure.yaml
execution:  slots: 8plugs:  - name: dut_serial    python: plugs.serial_port:DutSerial    scope: slot  - name: dmm    python: plugs.dmm:Keysight34461A    scope: execution    config:      address: "192.168.1.50"  - name: chamber    python: plugs.chamber:ThermalChamber    scope: station    config:      address: "192.168.1.60"

Déclarer un instrument partagé en execution ou station est ce qui le rend sûr. Une instance de plug exécute un appel de méthode à la fois. Quand plusieurs slots appellent le même plug partagé, leurs appels sont mis en file et exécutés un par un, comme des commandes sur une socket SCPI. La défaillance d'entrelacement décrite plus haut ne peut pas se produire via un plug partagé, puisqu'il y a une instance et un appel en cours.

L'erreur correspondante est de déclarer un instrument partagé en portée slot. Huit slots ouvrent alors huit sessions vers un seul instrument physique, sans aucune sérialisation, et vous obtenez exactement le problème d'entrelacement — de façon intermittente, sous charge, en production.

Garder la section critique dans un seul appel

La sérialisation est par appel de méthode, pas par phase. Une mesure partagée doit donc être un seul appel qui configure, déclenche et lit :

plugs/dmm.py
import pyvisaclass Keysight34461A:    def __init__(self, address: str):        self._rm = pyvisa.ResourceManager()        self._inst = self._rm.open_resource(f"TCPIP::{address}::INSTR")        self._inst.timeout = 5000    def measure_dc_volts(self, channel: int, expected_range: float) -> float:        """Sélectionner la voie, configurer et lire en un appel indivisible."""        self._inst.write(f"ROUT:CLOS (@{channel})")        self._inst.write(f"CONF:VOLT:DC {expected_range}")        return float(self._inst.query("READ?"))    def __del__(self):        self._inst.close()

Découpé en trois appels, l'appel d'un autre slot peut s'intercaler entre la configuration et la lecture :

plugs/dmm_wrong.py
class DmmWrong:    def select_channel(self, channel: int):        self._inst.write(f"ROUT:CLOS (@{channel})")    def configure(self, expected_range: float):        self._inst.write(f"CONF:VOLT:DC {expected_range}")    def read(self) -> float:        # Un autre slot a peut-être déjà reconfiguré le multiplexeur.        return float(self._inst.query("READ?"))

La phase passe alors sa propre voie, et l'instance partagée conserve chaque séquence intacte. Une phase lit sa position depuis run.slot_id, la clé déclarée dans execution.slots :

phases/measure_rail.py
CHANNELS = {"s1": 101, "s2": 102, "s3": 103, "s4": 104}def measure_rail(measurements, run, dmm):    measurements.rail_voltage = dmm.measure_dc_volts(        channel=CHANNELS[run.slot_id],        expected_range=10.0,    )

Associer explicitement les clés de slot aux voies, plutôt que de dériver une voie d'un numéro de slot, garde le câblage lisible et survit à une renumérotation du banc.

Garder les appels partagés courts

La mise en file a un coût direct sur le débit, et c'est la raison pour laquelle les postes parallèles n'atteignent pas leurs spécifications.

Une méthode de plug partagé qui dort retient les appels de tous les autres slots vers ce plug aussi longtemps qu'elle s'exécute. Le temps de stabilisation en est la cause habituelle :

plugs/dmm_slow.py
import timeclass DmmSlow:    def measure_after_settle(self, channel: int) -> float:        self._inst.write(f"ROUT:CLOS (@{channel})")        time.sleep(2.0)          # bloque les huit slots        return float(self._inst.query("READ?"))

Avec huit slots, cette stabilisation de deux secondes coûte seize secondes de temps de poste par cycle, et sept slots la passent inactifs. Mettez l'attente dans la phase, où les slots attendent simultanément :

phases/measure_rail.py
import timeCHANNELS = {"s1": 101, "s2": 102, "s3": 103, "s4": 104}def measure_rail(measurements, run, psu, dmm):    channel = CHANNELS[run.slot_id]    psu.enable(channel)    time.sleep(2.0)  # chaque slot attend en parallèle    measurements.rail_voltage = dmm.measure_dc_volts(channel, 10.0)

La règle : les méthodes de plug partagé ne devraient contenir que des E/S d'instrument, rien d'autre. L'attente, le calcul et les boucles de reprise appartiennent aux phases.

Dimensionner le bénéfice avant de construire

Savoir si le partage est acceptable relève de l'arithmétique, à faire avant la construction du banc.

Soit N le nombre de slots, S le temps d'instrument partagé par unité, et P le temps par slot qui s'exécute en parallèle. Le temps de cycle vaut approximativement :

model.txt
cycle ≈ max(P, N × S)

Huit slots, 25 s de travail parallèle, 1 s de mesure partagée :

good.txt
max(25, 8 × 1) = 25s     l'instrument partagé est gratuit

Le même poste avec une mesure partagée de 4 secondes :

bad.txt
max(25, 8 × 4) = 32s     le multimètre est maintenant le goulot

Au-delà de N × S > P, ajouter des slots n'apporte rien : le neuvième slot attend le multimètre. C'est le moment soit d'acheter un second instrument, soit de raccourcir la mesure partagée, soit d'arrêter d'ajouter des positions.

Utiliser phase_first avec les instruments partagés

Avec des slots configurés, la stratégie d'exécution influence le regroupement du travail partagé :

procedure.yaml
execution:  strategy: phase_first  slots: 8

phase_first, la valeur par défaut, exécute la même phase sur tous les slots avant de passer à la suivante. La mesure au multimètre de chaque slot se produit dans la même fenêtre, donc l'instrument reste dans une seule configuration pour le groupe.

slot_first termine un slot avant de démarrer le suivant, ce qui reconfigure l'instrument partagé à chaque changement. Pour un poste dont le partage est coûteux à reconfigurer, phase_first est le meilleur défaut.

Setup et teardown partagés

Certains matériels partagés ne sont pas mesurés par unité du tout : une enceinte monte en température une fois, une alimentation de rack s'allume une fois. Ce sont des phases de portée execution :

procedure.yaml
setup:  - name: Ramp Chamber to 85C    python: phases.ramp_chamber    scope: executionmain:  - name: Measure Leakage    python: phases.measure_leakageteardown:  - name: Discharge This Position    python: phases.discharge  - name: Return Chamber to Ambient    python: phases.cool_chamber    scope: execution

Le setup de portée execution s'exécute une fois avant le démarrage de tout slot ; le teardown de portée execution s'exécute une fois après la fin de tous les slots.

Cette portée est une exigence de correction, pas une commodité. Un teardown de portée slot peut s'exécuter pendant que ses voisins testent encore. Return Chamber to Ambient en portée slot commencerait à refroidir dès la fin de la première unité, et toutes les unités restantes seraient mesurées sur une rampe de température descendante. Ces unités échouent de façon intermittente, les échecs ne se reproduisent pas au banc, et rien dans les données ne désigne l'enceinte.

La règle en découle directement : si un teardown touche quelque chose qu'un autre slot peut encore utiliser, il doit être en scope: execution.

Comportement en cas d'échec

Un plug partagé qui échoue à s'initialiser arrête tous les slots, tout comme une phase de setup de portée execution en échec. C'est correct — huit unités testées sans enceinte à température produiraient huit réussites dénuées de sens — mais cela fait du setup partagé le code aux plus lourdes conséquences du poste.

Donnez à ces phases des reprises et des messages d'erreur qui nomment l'instrument :

procedure.yaml
setup:  - name: Ramp Chamber to 85C    python: phases.ramp_chamber    scope: execution    timeout: 15m    retry:      limit: 2      delay: 30s

Si la connexion à l'instrument tombe en cours de test, gérez la reconnexion dans le plug. Le moteur garantit un processus de plug durable, pas une session d'instrument vivante ; il vérifie la santé d'un plug station conservé avant chaque réutilisation et le relance si le processus est mort, mais une session TCP morte dans un processus vivant est à vous de détecter.

Pièges courants

Déclarer un instrument partagé en portée slot. N sessions vers un instrument, aucune sérialisation, commandes entrelacées. C'est la défaillance qui produit de mauvais nombres au lieu d'erreurs.

Découper une configuration-lecture en plusieurs méthodes de plug. Un autre slot peut s'intercaler dans l'intervalle.

Dormir dans une méthode de plug partagé. Tous les autres slots attendent. Déplacez l'attente dans la phase.

Nettoyer du matériel partagé dans un teardown de slot. Le premier slot qui termine perturbe les autres.

Dimensionner le banc sans le terme d'instrument partagé. Un banc à 16 slots avec une mesure partagée de 3 secondes est un cycle de 48 secondes, quelle que soit la rapidité des unités.

Points clés

  • Les instruments partagés échouent de deux façons : commandes entrelacées produisant de mauvaises données, et mise en file détruisant le débit.
  • scope: execution ou scope: station donne une instance dont les appels se sérialisent, ce qui empêche l'entrelacement par construction.
  • Une méthode de plug partagé doit être toute la section critique : configurer, déclencher et lire en un appel.
  • Gardez les attentes hors des méthodes partagées ; une méthode qui dort bloque tous les slots.
  • Le temps de cycle vaut approximativement max(P, N × S) — au-delà de ce point de bascule, ajouter des slots n'apporte rien.
  • Tout ce qu'un slot qui termine pourrait perturber appartient à un teardown de portée execution.

Plus de guides

Mettez ce guide en pratique