Le test hardware-in-the-loop (HIL) valide le firmware embarqué en stimulant les entrées matérielles réelles et en mesurant les sorties réelles. Contrairement à la simulation pure, les tests HIL s'exécutent sur le matériel cible réel avec de vrais périphériques. Cela détecte des bugs que la simulation manque : problèmes de timing, bruit ADC, conflits d'interruptions et problèmes d'initialisation des périphériques. Ce guide vous montre comment construire un système de test HIL basé sur Python avec OpenHTF et TofuPilot.
HIL vs SIL vs MIL
| Méthode | Matériel | Logiciel | Fidélité | Vitesse | Coût |
|---|---|---|---|---|---|
| MIL (Model-in-the-Loop) | Aucun | Plante + contrôleur simulés | Faible | Rapide (secondes) | Faible |
| SIL (Software-in-the-Loop) | Aucun | Code contrôleur réel sur hôte | Moyenne | Rapide (secondes) | Faible |
| HIL (Hardware-in-the-Loop) | Carte cible réelle | Firmware réel | Élevée | Temps réel | Moyen |
| Test physique | Cible réelle + plante réelle | Firmware réel | La plus élevée | Temps réel | Élevé |
Le HIL se situe entre le SIL et le test physique. Vous testez du firmware réel sur du matériel réel, mais simulez l'environnement (capteurs, actionneurs, systèmes externes).
Quand utiliser le test HIL
- Tests de régression firmware. Chaque build de firmware subit les mêmes tests physiques. Détecte les régressions que les tests unitaires manquent.
- Validation des périphériques. GPIO, ADC, DAC, PWM, UART, SPI, I2C. Le silicium réel se comporte différemment des modèles de simulation.
- Systèmes critiques pour la sécurité. L'automobile, le médical et l'aérospatial exigent des tests HIL pour la certification (ISO 26262, IEC 62304).
- Validation pré-production. Validez le firmware sur du matériel réel avant de passer en production.
Architecture de test HIL
Une configuration HIL typique en Python :
| Composant | Rôle | Exemple |
|---|---|---|
| Carte cible (DUT) | Exécute le firmware sous test | Carte de dev STM32, ESP32, Raspberry Pi Pico |
| Hôte de test | Exécute les scripts de test Python | PC ou Raspberry Pi |
| DAQ / Adaptateur GPIO | Stimuler les entrées, lire les sorties | Arduino, LabJack, NI DAQ |
| Alimentation | Alimentation contrôlée du DUT | Alimentation de banc (programmable) |
| Série/débogage | Flasher le firmware, envoyer des commandes | USB-to-serial, J-Link, ST-Link |
| Instruments | Mesures de précision | Multimètre, oscilloscope |
Étape 1 : Créer les plugs d'interface matérielle
Chaque équipement de test obtient un Plug OpenHTF. Cela garde les phases de test propres et indépendantes de l'instrument.
import openhtf as htf
from openhtf.plugs import BasePlug
class GpioPlug(BasePlug):
"""Interface GPIO pour stimuler les entrées du DUT et lire les sorties.
Remplacez par un vrai adaptateur GPIO (ex. LabJack, Arduino, RPi.GPIO).
"""
def setUp(self):
self._pin_states = {}
def set_pin(self, pin: int, state: bool):
"""Définir une broche GPIO sur le montage de test (entrée DUT)."""
self._pin_states[pin] = state
def read_pin(self, pin: int) -> bool:
"""Lire une broche GPIO du DUT (sortie DUT)."""
return self._pin_states.get(pin, False)
def tearDown(self):
self._pin_states.clear()
class AdcPlug(BasePlug):
"""Interface ADC pour lire les sorties analogiques du DUT.
Remplacez par un vrai DAQ (ex. NI DAQmx, LabJack, ADS1115).
"""
def setUp(self):
self._channels = {0: 1.65, 1: 2.50, 2: 0.33}
def read_channel(self, channel: int) -> float:
"""Lire la tension d'un canal ADC."""
return self._channels.get(channel, 0.0)
def tearDown(self):
pass
class PwmPlug(BasePlug):
"""Interface de mesure PWM.
Remplacez par un vrai compteur de fréquence ou oscilloscope.
"""
def setUp(self):
pass
def measure_frequency(self) -> float:
"""Mesurer la fréquence de sortie PWM en Hz."""
return 1000.0
def measure_duty_cycle(self) -> float:
"""Mesurer le rapport cyclique PWM en pourcentage."""
return 50.0
def tearDown(self):
passÉtape 2 : Écrire les tests GPIO
Le test HIL le plus simple : définir une entrée, vérifier que le firmware pilote la bonne sortie.
import openhtf as htf
@htf.measures(
htf.Measurement("gpio_loopback")
.equals(True)
.doc("Bouclage GPIO : définir l'entrée, vérifier que la sortie suit"),
)
@htf.plug(gpio=GpioPlug)
def test_gpio_loopback(test, gpio):
"""Vérifier que le firmware route correctement l'entrée GPIO vers la sortie."""
gpio.set_pin(1, True) # Stimuler l'entrée du DUT
readback = gpio.read_pin(1) # Lire la sortie du DUT
test.measurements.gpio_loopback = readbackÉtape 3 : Écrire les tests ADC/capteur
Simulez les entrées capteur et vérifiez que le DUT les lit correctement.
import openhtf as htf
from openhtf.util import units
@htf.measures(
htf.Measurement("adc_channel_0")
.in_range(1.5, 1.8)
.with_units(units.VOLT)
.doc("ADC ch0 : tension de référence 1,65V"),
htf.Measurement("adc_channel_1")
.in_range(2.3, 2.7)
.with_units(units.VOLT)
.doc("ADC ch1 : sortie capteur 2,5V"),
)
@htf.plug(adc=AdcPlug)
def test_adc_readings(test, adc):
"""Vérifier que les canaux ADC lisent les tensions attendues."""
test.measurements.adc_channel_0 = adc.read_channel(0)
test.measurements.adc_channel_1 = adc.read_channel(1)Étape 4 : Écrire les tests PWM
Vérifiez que le firmware génère le bon signal PWM (fréquence et rapport cyclique).
import openhtf as htf
from openhtf.util import units
@htf.measures(
htf.Measurement("pwm_frequency")
.in_range(950, 1050)
.with_units(units.HERTZ)
.doc("Fréquence de sortie PWM (cible : 1 kHz)"),
htf.Measurement("pwm_duty_cycle")
.in_range(48, 52)
.doc("Rapport cyclique PWM en pourcentage (cible : 50%)"),
)
@htf.plug(pwm=PwmPlug)
def test_pwm_output(test, pwm):
"""Vérifier la fréquence et le rapport cyclique de la sortie PWM."""
test.measurements.pwm_frequency = pwm.measure_frequency()
test.measurements.pwm_duty_cycle = pwm.measure_duty_cycle()Étape 5 : Assembler le test HIL
Connectez toutes les phases avec TofuPilot pour suivre les régressions de versions firmware dans le temps.
import openhtf as htf
from tofupilot.openhtf import TofuPilot
def main():
test = htf.Test(
test_gpio_loopback,
test_adc_readings,
test_pwm_output,
procedure_id="HIL-001",
part_number="ECU-100",
)
with TofuPilot(test):
test.execute(test_start=lambda: input("Version du firmware : "))
if __name__ == "__main__":
main()Utilisez l'invite de numéro de série pour saisir la version du firmware. Cela permet à TofuPilot de suivre quels builds de firmware passent ou échouent à chaque test, construisant un historique de régression.
HIL en CI/CD
La vraie puissance du test HIL vient de son exécution automatique à chaque build de firmware.
| Étape CI/CD | Action |
|---|---|
| 1. Push de code | Le développeur pousse une modification firmware |
| 2. Build | Le CI compile le binaire firmware |
| 3. Flash | Le CI flashe le binaire sur la carte cible (via J-Link/ST-Link) |
| 4. Test HIL | Le CI exécute le script de test OpenHTF |
| 5. Résultats | TofuPilot enregistre le pass/fail par mesure |
| 6. Gate | Le CI échoue le build si une mesure est hors spécification |
Cela nécessite une station de test dédiée connectée à votre runner CI. La station de test comprend la carte cible, l'adaptateur GPIO, le DAQ et les instruments. Le runner CI (Jenkins, GitHub Actions self-hosted runner, etc.) déclenche le test après chaque build.
Catégories courantes de tests HIL
| Catégorie | Quoi tester | Mesures |
|---|---|---|
| GPIO | Routage entrée/sortie, réponse aux interruptions | État des broches, temps de réponse |
| ADC | Précision de lecture capteur, bruit | Valeurs de tension, plancher de bruit |
| DAC | Précision de la tension de sortie | Valeurs de tension |
| PWM | Fréquence, rapport cyclique, résolution | Fréquence, rapport cyclique |
| Communication | Intégrité des données UART, SPI, I2C, CAN | Taux d'erreur binaire, temps de réponse |
| Timing | Latence d'interruption, ordonnancement des tâches | Temps de réponse en microsecondes |
| Alimentation | Courant en mode veille, temps de réveil | Consommation de courant, latence de réveil |
| Watchdog | Récupération après blocage | Détection de reset, temps de récupération |
HIL vs FCT en production
Le HIL et le FCT testent des choses différentes à des étapes différentes :
| HIL | FCT | |
|---|---|---|
| Quand | Développement, à chaque build firmware | Production, à chaque carte fabriquée |
| Quoi | Comportement firmware sur matériel réel | Assemblage de la carte et fonction système |
| DUT | Carte de développement ou prototype | Carte de production |
| Ce qui change entre les tests | Firmware (modifications de code) | Matériel (variation de fabrication) |
| Un échec signifie | Bug firmware (correction de code) | Défaut d'assemblage (reprise ou mise au rebut) |
| Fréquence d'exécution | À chaque build CI | À chaque unité fabriquée |
| Utilisation TofuPilot | Suivre les régressions entre versions firmware | Suivre le FPY, Cpk, cartes de contrôle |
De nombreuses équipes utilisent le même framework de test OpenHTF pour le HIL et le FCT. Les plugs changent (GPIO carte de dev vs. montage de production), mais la structure de test reste la même.