Test Types & Methods

Tests HIL pour systèmes embarqués

Configurez les tests hardware-in-the-loop pour systèmes embarqués avec Python, le contrôle GPIO et la journalisation TofuPilot.

JJulien Buteau
intermediate12 min de lecture14 mars 2026

Le test hardware-in-the-loop (HIL) permet de vérifier le firmware embarqué sur du matériel réel sans avoir à sonder manuellement avec un multimètre. Ce guide vous accompagne dans la construction d'un test HIL en Python qui contrôle des GPIO, lit des signaux analogiques, communique avec le DUT par liaison série et enregistre tout dans TofuPilot.

Prérequis

  • Python 3.8+
  • OpenHTF installé (pip install openhtf)
  • Client Python TofuPilot (pip install tofupilot)
  • Un DUT avec interfaces GPIO, ADC et UART
  • Un montage de test (Raspberry Pi, NI DAQ ou matériel d'E/S similaire)
  • PySerial (pip install pyserial)

Qu'est-ce que le test HIL ?

Dans une configuration HIL, votre DUT est connecté à un montage de test qui simule les signaux réels qu'il verrait en production. Le montage génère des stimuli (niveaux de tension, impulsions numériques, commandes série) et mesure les réponses du DUT. Votre script de test orchestre toute la séquence.

Un banc HIL typique se compose de :

  • Hôte de test (PC ou Raspberry Pi) qui exécute le script de test
  • E/S numériques (broches GPIO ou carte d'E/S numérique) pour basculer les entrées et lire les sorties du DUT
  • E/S analogiques (DAQ ou ADC/DAC) pour générer des stimuli analogiques et mesurer les sorties du DUT
  • Liaison série (UART, SPI, I2C) pour envoyer des commandes et lire l'état du firmware du DUT

Le script de test pilote les trois canaux, vérifie le comportement du DUT à chaque étape et enregistre les résultats pass/fail.

Étape 1 : Contrôler les E/S numériques avec GPIO

La plupart des configurations HIL ont besoin de basculer les entrées du DUT et lire ses sorties. Si vous utilisez un Raspberry Pi comme hôte de test, RPi.GPIO fonctionne très bien. Pour une configuration sur PC, remplacez-le par le SDK de votre carte d'E/S.

hil_test/gpio_plug.py
import openhtf as htf
from openhtf.plugs import BasePlug

try:
    import RPi.GPIO as GPIO
except ImportError:
    GPIO = None  # Permet le développement sur des machines non-Pi

class GpioPowerPlug(BasePlug):
    """Contrôle l'alimentation du DUT et lit les broches d'état numériques."""

    POWER_PIN = 17
    STATUS_PIN = 27

    def setUp(self):
        if GPIO is None:
            raise RuntimeError("RPi.GPIO non disponible sur cette plateforme")
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.POWER_PIN, GPIO.OUT, initial=GPIO.LOW)
        GPIO.setup(self.STATUS_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

    def power_on(self):
        GPIO.output(self.POWER_PIN, GPIO.HIGH)

    def power_off(self):
        GPIO.output(self.POWER_PIN, GPIO.LOW)

    def read_status(self):
        return GPIO.input(self.STATUS_PIN)

    def tearDown(self):
        GPIO.output(self.POWER_PIN, GPIO.LOW)
        GPIO.cleanup([self.POWER_PIN, self.STATUS_PIN])

Le plug gère automatiquement l'initialisation et le nettoyage. OpenHTF appelle setUp() avant la première phase qui l'utilise et tearDown() après la fin du test, garantissant que votre DUT est toujours éteint proprement.

Étape 2 : Lire les signaux analogiques avec un DAQ

Pour les mesures analogiques, vous aurez besoin d'un ADC ou DAQ. Cet exemple utilise un DAQ MCC (via la bibliothèque uldaq), mais le schéma fonctionne de la même façon avec NI-DAQmx, Labjack ou un ADS1115 sur I2C.

hil_test/analog_plug.py
from openhtf.plugs import BasePlug
from uldaq import DaqDevice, InterfaceType, AiInputMode, Range


class AnalogInputPlug(BasePlug):
    """Lit les tensions analogiques depuis un DAQ USB."""

    def setUp(self):
        devices = DaqDevice.get_inventory(InterfaceType.USB)
        if not devices:
            raise RuntimeError("Aucun périphérique DAQ trouvé")
        self.daq = DaqDevice(devices[0])
        self.daq.connect()
        self.ai = self.daq.get_ai_device()

    def read_voltage(self, channel: int) -> float:
        """Lire une tension single-ended depuis le canal spécifié."""
        return self.ai.a_in(channel, AiInputMode.SINGLE_ENDED, Range.BIP10VOLTS, 0)

    def tearDown(self):
        self.daq.disconnect()
        self.daq.release()

Étape 3 : Communiquer avec le DUT par liaison série

La plupart des cibles embarquées exposent une console de débogage UART ou une interface de commande. Encapsulez-la dans un plug pour qu'OpenHTF gère le cycle de vie de la connexion.

hil_test/serial_plug.py
import time
import serial
from openhtf.plugs import BasePlug


class SerialCommandPlug(BasePlug):
    """Envoie des commandes au DUT via UART et lit les réponses."""

    PORT = "/dev/ttyUSB0"
    BAUDRATE = 115200
    TIMEOUT = 2.0

    def setUp(self):
        self.ser = serial.Serial(self.PORT, self.BAUDRATE, timeout=self.TIMEOUT)
        time.sleep(0.5)  # Attendre le bootloader du DUT
        self.ser.reset_input_buffer()

    def send_command(self, cmd: str) -> str:
        """Envoyer une commande et retourner la ligne de réponse."""
        self.ser.write(f"{cmd}\r
".encode())
        return self.ser.readline().decode().strip()

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

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

Étape 4 : Écrire les phases OpenHTF

Chaque phase de test exerce un aspect du DUT. L'injection de plug utilise le décorateur @htf.plug.

hil_test/phases.py
import time
import openhtf as htf
from openhtf.util import units

from hil_test.gpio_plug import GpioPowerPlug
from hil_test.analog_plug import AnalogInputPlug
from hil_test.serial_plug import SerialCommandPlug


@htf.plug(gpio=GpioPowerPlug)
@htf.measures(
    htf.Measurement("boot_status").equals(1)
)
def power_on_and_check_boot(test, gpio):
    """Mettre sous tension le DUT et vérifier qu'il démarre."""
    gpio.power_on()
    time.sleep(2.0)  # Attendre le démarrage
    test.measurements.boot_status = gpio.read_status()


@htf.plug(serial=SerialCommandPlug)
@htf.measures(
    htf.Measurement("firmware_version")
)
def read_firmware_version(test, serial):
    """Interroger la version du firmware du DUT via UART."""
    version = serial.get_firmware_version()
    test.measurements.firmware_version = version


@htf.plug(gpio=GpioPowerPlug, adc=AnalogInputPlug)
@htf.measures(
    htf.Measurement("vout_3v3").in_range(minimum=3.1, maximum=3.5).with_units(units.VOLT),
    htf.Measurement("vout_5v0").in_range(minimum=4.75, maximum=5.25).with_units(units.VOLT),
)
def measure_power_rails(test, gpio, adc):
    """Vérifier que les rails d'alimentation du DUT sont dans les spécifications."""
    test.measurements.vout_3v3 = adc.read_voltage(channel=0)
    test.measurements.vout_5v0 = adc.read_voltage(channel=1)


@htf.plug(serial=SerialCommandPlug, adc=AnalogInputPlug)
@htf.measures(
    htf.Measurement("dac_output_1v0").in_range(minimum=0.95, maximum=1.05).with_units(units.VOLT),
)
def test_dac_output(test, serial, adc):
    """Commander le DUT pour sortir 1.0V sur son DAC, puis mesurer."""
    serial.send_command("DAC SET 1000")  # 1000 mV
    time.sleep(0.5)
    test.measurements.dac_output_1v0 = adc.read_voltage(channel=2)


@htf.plug(gpio=GpioPowerPlug)
def power_off(test, gpio):
    """Éteindre le DUT."""
    gpio.power_off()

Étape 5 : Intégrer avec TofuPilot

Encapsulez l'exécution du test OpenHTF dans TofuPilot pour enregistrer automatiquement les résultats, les mesures et les métadonnées du DUT.

hil_test/main.py
import openhtf as htf
from tofupilot.openhtf import TofuPilot

from hil_test.phases import (
    power_on_and_check_boot,
    read_firmware_version,
    measure_power_rails,
    test_dac_output,
    power_off,
)

test = htf.Test(
    power_on_and_check_boot,
    read_firmware_version,
    measure_power_rails,
    test_dac_output,
    power_off,
)

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

Chaque exécution de test est envoyée à TofuPilot avec le numéro de série du DUT, toutes les mesures et limites, et le statut pass/fail par phase.

Dépannage

SymptômeCause probableSolution
RuntimeError: RPi.GPIO not availableExécution sur une machine non-PiUtilisez un simulateur GPIO ou déployez sur le Pi
serial.SerialException: could not open portMauvais port ou DUT non alimentéVérifiez la constante PORT, vérifiez que le DUT est alimenté
No DAQ device foundDAQ non connecté ou pilote manquantExécutez lsusb pour vérifier, installez les pilotes uldaq
L'ADC lit 0V sur tous les canauxMauvais mode d'entrée ou plageConfirmez SINGLE_ENDED vs DIFFERENTIAL, vérifiez le câblage
Le DUT ne répond pas aux commandes sérieDébit en bauds incorrect ou mauvais terminateur de ligneFaites correspondre les paramètres du firmware du DUT, essayez `\r
vs
`
Les mesures échouent par intermittenceTemps de stabilisation trop court après le stimulusAugmentez time.sleep() après les changements d'état

Plus de guides

Mettez ce guide en pratique