Instrument Control

Contrôler plusieurs instruments de test

Apprenez à orchestrer plusieurs instruments (alimentation, DMM, port série) dans un seul test OpenHTF à l'aide de plugs, avec gestion automatique du cycle.

JJulien Buteau
intermediate10 min de lecture14 mars 2026

Les tests fonctionnels impliquent rarement un seul instrument. Un vrai test de carte alimente le DUT via une alimentation, mesure tension et courant avec un DMM, et valide la communication firmware via un port série. OpenHTF gère cela grâce au système de plugs : chaque instrument a son propre plug, les phases déclarent les plugs dont elles ont besoin, et OpenHTF gère le cycle de vie et l'injection.

Prérequis

  • Python 3.8+
  • Compte TofuPilot et clé API
  • OpenHTF et TofuPilot installés (pip install openhtf tofupilot)
  • Pilotes d'instruments (pyvisa, pyserial)

Pourquoi les tests multi-instruments bénéficient des plugs

ApprocheNettoyagePartage entre phasesTestabilité
Variables globalesManuel, sujet aux erreursImpliciteDifficile à simuler
Passer les instruments en argumentsManuelExplicite mais verbeuxModérée
Plugs OpenHTFAutomatique via tearDownInjection déclarativeFacile à simuler

Chaque plug est une classe qui possède une connexion instrument. OpenHTF l'instancie une fois par exécution de test, l'injecte dans chaque phase qui le demande, et appelle tearDown quand le test se termine.

Étape 1 : Définir un plug par instrument

plugs/psu_plug.py
import openhtf as htf
import pyvisa

class PSUPlug(htf.plugs.BasePlug):
    """Contrôle une alimentation de table compatible SCPI."""

    RESOURCE = "USB0::0x1AB1::0x0E11::DP8B213601234::INSTR"

    def setUp(self):
        rm = pyvisa.ResourceManager()
        self._instrument = rm.open_resource(self.RESOURCE)
        self._instrument.timeout = 5000

    def enable_output(self, channel: int, voltage: float, current_limit: float):
        self._instrument.write(f"INST CH{channel}")
        self._instrument.write(f"VOLT {voltage}")
        self._instrument.write(f"CURR {current_limit}")
        self._instrument.write("OUTP ON")

    def disable_output(self, channel: int):
        self._instrument.write(f"INST CH{channel}")
        self._instrument.write("OUTP OFF")

    def measure_voltage(self, channel: int) -> float:
        self._instrument.write(f"INST CH{channel}")
        return float(self._instrument.query("MEAS:VOLT?"))

    def measure_current(self, channel: int) -> float:
        self._instrument.write(f"INST CH{channel}")
        return float(self._instrument.query("MEAS:CURR?"))

    def tearDown(self):
        self._instrument.write("OUTP:ALL OFF")
        self._instrument.close()
plugs/dmm_plug.py
import openhtf as htf
import pyvisa

class DMMPlug(htf.plugs.BasePlug):
    """Contrôle un DMM 6,5 digits via GPIB."""

    RESOURCE = "GPIB0::22::INSTR"

    def setUp(self):
        rm = pyvisa.ResourceManager()
        self._instrument = rm.open_resource(self.RESOURCE)
        self._instrument.timeout = 10000
        self._instrument.write("*RST")

    def measure_dc_voltage(self) -> float:
        self._instrument.write("CONF:VOLT:DC")
        return float(self._instrument.query("READ?"))

    def measure_resistance(self) -> float:
        self._instrument.write("CONF:RES")
        return float(self._instrument.query("READ?"))

    def tearDown(self):
        self._instrument.write("*RST")
        self._instrument.close()
plugs/serial_plug.py
import openhtf as htf
import serial
import time

class SerialPlug(htf.plugs.BasePlug):
    """Communique avec le firmware du DUT via UART."""

    PORT = "/dev/ttyUSB0"
    BAUDRATE = 115200

    def setUp(self):
        self._port = serial.Serial(
            port=self.PORT,
            baudrate=self.BAUDRATE,
            timeout=2.0
        )
        time.sleep(0.1)

    def send_command(self, command: str) -> str:
        self._port.write(f"{command}\r
".encode())
        return self._port.readline().decode().strip()

    def get_firmware_version(self) -> str:
        return self.send_command("VERSION?")

    def run_self_test(self) -> bool:
        return self.send_command("SELFTEST") == "PASS"

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

Étape 2 : Écrire des phases qui déclarent leurs plugs

Chaque phase reçoit les plugs dont elle a besoin via le décorateur @htf.plug. Les phases ne déclarent que les instruments qu'elles utilisent réellement.

phases/power_phases.py
import time
import openhtf as htf
from openhtf.util import units
from plugs.psu_plug import PSUPlug
from plugs.dmm_plug import DMMPlug

@htf.plug(psu=PSUPlug)
@htf.measures(
    htf.Measurement("supply_voltage_v12")
    .in_range(minimum=11.8, maximum=12.2)
    .with_units(units.VOLT),
    htf.Measurement("supply_current")
    .in_range(minimum=0.05, maximum=2.0)
    .with_units(units.AMPERE),
)
def power_on_dut(test, psu):
    """Activer le rail 12 V et vérifier que l'alimentation est dans les tolérances."""
    psu.enable_output(channel=1, voltage=12.0, current_limit=2.5)
    time.sleep(0.5)
    test.measurements.supply_voltage_v12 = psu.measure_voltage(channel=1)
    test.measurements.supply_current = psu.measure_current(channel=1)


@htf.plug(psu=PSUPlug, dmm=DMMPlug)
@htf.measures(
    htf.Measurement("output_rail_3v3")
    .in_range(minimum=3.267, maximum=3.333)
    .with_units(units.VOLT),
    htf.Measurement("load_resistance")
    .in_range(minimum=95.0, maximum=105.0)
    .with_units(units.OHM),
)
def verify_power_rails(test, psu, dmm):
    """Vérification croisée du rail 3,3 V embarqué avec une mesure DMM externe."""
    test.measurements.output_rail_3v3 = dmm.measure_dc_voltage()
    test.measurements.load_resistance = dmm.measure_resistance()
phases/firmware_phases.py
import openhtf as htf
from plugs.serial_plug import SerialPlug

EXPECTED_FW_VERSION = "2.4.1"

@htf.plug(serial=SerialPlug)
@htf.measures(
    htf.Measurement("firmware_version").equals(EXPECTED_FW_VERSION),
    htf.Measurement("self_test_passed").equals(True),
)
def verify_firmware(test, serial):
    """Vérifier la version firmware et exécuter l'autotest embarqué."""
    test.measurements.firmware_version = serial.get_firmware_version()
    test.measurements.self_test_passed = serial.run_self_test()


@htf.plug(serial=SerialPlug)
@htf.measures(
    htf.Measurement("adc_reading_mv")
    .in_range(minimum=1180, maximum=1220),
)
def verify_adc_calibration(test, serial):
    """Commander au DUT de rapporter sa lecture ADC de la référence 1,2 V."""
    response = serial.send_command("ADC:REF?")
    test.measurements.adc_reading_mv = float(response)

Étape 3 : Séquencer les phases dans le bon ordre

L'ordre des instruments est important. Alimentez le DUT avant de communiquer avec son firmware.

phase_order.txt
power_on_dut → verify_power_rails → verify_firmware → verify_adc_calibration
      ↑ PSU seul       ↑ PSU + DMM          ↑ Série seul       ↑ Série seul

OpenHTF instancie chaque plug une seule fois et réutilise la même instance dans toutes les phases qui le demandent. Quand power_on_dut et verify_power_rails déclarent tous deux psu=PSUPlug, ils reçoivent la même instance de PSUPlug.

Étape 4 : Assembler et exécuter le test complet

test_board_functional.py
import openhtf as htf
from tofupilot.openhtf import TofuPilot

from phases.power_phases import power_on_dut, verify_power_rails
from phases.firmware_phases import verify_firmware, verify_adc_calibration

def main():
    test = htf.Test(
        power_on_dut,
        verify_power_rails,
        verify_firmware,
        verify_adc_calibration,
        test_name="Board Functional Test",
    )

    with TofuPilot(test):
        test.execute(test_start=lambda: input("Entrer le numéro de série du DUT : "))

if __name__ == "__main__":
    main()

À l'exécution, OpenHTF :

  1. Instancie PSUPlug, DMMPlug et SerialPlug avant la première phase
  2. Exécute les phases dans l'ordre, en injectant les instances partagées
  3. Appelle tearDown sur les trois plugs après la dernière phase, quel que soit le résultat réussite/échec
  4. TofuPilot envoie l'exécution complète avec toutes les mesures

Ordre de nettoyage et sécurité

Les plugs se nettoient dans l'ordre inverse d'instanciation par défaut. Concevez votre nettoyage pour qu'il soit sûr quel que soit l'ordre : désactivez toujours les sorties dans tearDown, ne supposez jamais qu'un autre plug est encore actif.

plugs/psu_plug.py
def tearDown(self):
    try:
        self._instrument.write("OUTP:ALL OFF")
        self._instrument.close()
    except Exception:
        pass  # l'instrument peut être déjà déconnecté

Partager les instruments entre fichiers de phase

Comme OpenHTF crée une seule instance de plug par classe par exécution de test, vous pouvez répartir les phases dans plusieurs fichiers tout en partageant la même connexion instrument.

test_board_functional.py
# Les deux fichiers importent la même classe PSUPlug.
# OpenHTF injecte la même instance dans les deux phases.
from phases.power_phases import power_on_dut, verify_power_rails
from phases.stress_phases import run_load_step  # utilise aussi PSUPlug

Cela fonctionne sans aucune configuration supplémentaire.

Comparaison : Structure de test mono vs. multi-instruments

AspectUn seul instrumentPlusieurs instruments
Nombre de plugs11 par instrument
Déclarations de phase@htf.plug(inst=MyPlug)Chaque phase ne déclare que ce dont elle a besoin
NettoyageAutomatiqueAutomatique, un tearDown par plug
SéquencementN/AAlimenter avant le firmware, désactiver dans l'ordre inverse
Upload TofuPilotToutes les mesures d'un seul plugToutes les mesures de tous les plugs, unifiées

Plus de guides

Mettez ce guide en pratique