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.
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.
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.
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.
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.
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ôme | Cause probable | Solution |
|---|---|---|
RuntimeError: RPi.GPIO not available | Exécution sur une machine non-Pi | Utilisez un simulateur GPIO ou déployez sur le Pi |
serial.SerialException: could not open port | Mauvais port ou DUT non alimenté | Vérifiez la constante PORT, vérifiez que le DUT est alimenté |
No DAQ device found | DAQ non connecté ou pilote manquant | Exécutez lsusb pour vérifier, installez les pilotes uldaq |
| L'ADC lit 0V sur tous les canaux | Mauvais mode d'entrée ou plage | Confirmez SINGLE_ENDED vs DIFFERENTIAL, vérifiez le câblage |
| Le DUT ne répond pas aux commandes série | Débit en bauds incorrect ou mauvais terminateur de ligne | Faites correspondre les paramètres du firmware du DUT, essayez `\r |
vs | ||
| ` | ||
| Les mesures échouent par intermittence | Temps de stabilisation trop court après le stimulus | Augmentez time.sleep() après les changements d'état |