Tests embarqués¶
Valider le firmware avant de le déployer — des tests unitaires sur host jusqu'au HIL branché sur le vrai matériel.
Le défi des tests en embarqué¶
Tester du firmware est plus complexe que tester du logiciel applicatif. Le code est étroitement couplé au matériel (registres, interruptions, périphériques), les environnements d'exécution sont hétérogènes (MCU, Linux embarqué, simulateur), et les boucles de feedback sont lentes — flasher une cible prend plusieurs secondes.
La tentation est de ne tester qu'une fois sur la cible réelle. C'est une stratégie coûteuse : les bugs trouvés tardivement sont exponentiellement plus chers à corriger, et certains bugs (conditions de course, comportements sous charge) ne se manifestent qu'en production.
Pyramide de tests embarqués¶
La pyramide de tests s'applique au firmware, avec des niveaux adaptés aux contraintes matérielles :
graph TD
A["🔺 Tests système\n(HIL + cible réelle)\nValidation end-to-end\nLents, coûteux, peu nombreux"] --> B
B["🔷 Tests d'intégration\n(simulation QEMU/Renode)\nInteraction composants\nMoyennement rapides"] --> C
C["🟩 Tests unitaires sur host\n(mock HAL, natif desktop)\nLogique applicative isolée\nRapides, nombreux, bon retour CI"]
style A fill:#6e1a1a,color:#fff,stroke:#fff
style B fill:#6e4a1a,color:#fff,stroke:#fff
style C fill:#1a6e3a,color:#fff,stroke:#fff La base large représente le plus grand nombre de tests — rapides, exécutables en quelques secondes sur la machine de développement. Le sommet est réservé aux validations complètes sur matériel réel, exécutées moins fréquemment.
Tests unitaires sur cible — Unity¶
Unity est le framework de tests unitaires en C le plus utilisé en embarqué. Il est petit (3 fichiers C), sans dépendances, et tourne sur n'importe quelle cible MCU disposant d'une sortie série.
// test_capteur_sht40.c
#include "unity.h"
#include "capteur_sht40.h"
void setUp(void) {
capteurSHT40_init(I2C_PORT_1);
}
void tearDown(void) {
capteurSHT40_deinit();
}
void test_lecture_temperature_plage_valide(void) {
float temperature;
CapteurStatus status = capteurSHT40_lireTemperature(&temperature);
TEST_ASSERT_EQUAL(CAPTEUR_OK, status);
TEST_ASSERT_FLOAT_WITHIN(0.1f, 25.0f, temperature); // ±0.1 °C autour de 25 °C
}
void test_adresse_i2c_incorrecte_retourne_erreur(void) {
capteurSHT40_setAdresse(0x99); // adresse invalide
float t;
TEST_ASSERT_EQUAL(CAPTEUR_ERR_I2C, capteurSHT40_lireTemperature(&t));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_lecture_temperature_plage_valide);
RUN_TEST(test_adresse_i2c_incorrecte_retourne_erreur);
return UNITY_END();
}
CppUTest est l'équivalent pour C++, avec une API inspirée de JUnit. Il supporte les mocks natifs et une meilleure gestion des exceptions.
Tests unitaires sur host — mock du HAL¶
La technique la plus productive pour les tests rapides : compiler le firmware pour la machine de développement (x86_64) en remplaçant les appels au HAL matériel par des mocks (implémentations de substitution contrôlables).
Architecture avec HAL découplé¶
firmware/
├── app/
│ ├── logique_alarme.c ← logique pure, sans HAL direct
│ └── logique_alarme.h
├── hal/
│ ├── hal_gpio.h ← interface abstraite
│ ├── hal_gpio_stm32.c ← implémentation STM32
│ └── hal_gpio_mock.c ← mock pour tests sur host
└── tests/
└── test_logique_alarme.c
// hal_gpio_mock.c — implémentation mock contrôlable depuis les tests
static uint8_t gpio_values[HAL_GPIO_MAX] = {0};
void hal_gpio_write(uint8_t pin, uint8_t valeur) {
gpio_values[pin] = valeur;
}
uint8_t hal_gpio_read(uint8_t pin) {
return gpio_values[pin];
}
// Helpers pour les tests
void hal_gpio_mock_set(uint8_t pin, uint8_t valeur) {
gpio_values[pin] = valeur;
}
uint8_t hal_gpio_mock_get(uint8_t pin) {
return gpio_values[pin];
}
// test_logique_alarme.c — tourne sur host (cmake natif)
#include "unity.h"
#include "logique_alarme.h"
#include "hal_gpio_mock.h"
void test_alarme_se_declenche_si_temperature_haute(void) {
hal_gpio_mock_set(PIN_CAPTEUR_ALARME, 1); // simule capteur actif
alarme_traiter();
TEST_ASSERT_EQUAL(1, hal_gpio_mock_get(PIN_LED_ALARME));
TEST_ASSERT_EQUAL(1, hal_gpio_mock_get(PIN_BUZZER));
}
Ce pattern permet d'exécuter des centaines de tests en quelques millisecondes sans aucun matériel.
HIL — Hardware-in-the-Loop¶
Le HIL (Hardware-in-the-Loop) est le niveau de test où le firmware tourne sur le vrai matériel, mais les entrées/sorties physiques sont simulées par un système de test automatisé.
Architecture HIL¶
graph TD
subgraph Rack["Rack HIL"]
DUT["DUT (Device Under Test)<br/>Carte cible + firmware"] <-->|GPIO, DAC, CAN| SIM["Simulateur d'entrées<br/>GPIO → relais<br/>DAC → tensions analog.<br/>CAN → messages CAN"]
DUT -->|USB / UART / Ethernet| SRV["Serveur de test<br/>(Raspberry Pi / PC)<br/>Python pytest + pyvisa + pyserial"]
end # test_hil_alarme.py — pytest avec pilotage HIL
import pytest
import serial
import time
from hil_driver import HILDriver
@pytest.fixture
def hil():
driver = HILDriver(port='/dev/ttyUSB0')
driver.reset_dut()
yield driver
driver.cleanup()
def test_alarme_temperature(hil):
# Simule une température de 85 °C via DAC
hil.set_analog(channel=0, voltage=3.2) # 3.2V = 85 °C sur ce capteur
time.sleep(0.5) # laisse le firmware réagir
# Vérifie que la LED d'alarme s'est allumée
assert hil.read_gpio(pin='LED_ALARME') == 1
# Vérifie le message série
assert hil.wait_serial_message("ALARME_TEMP", timeout=1.0)
Simulation — QEMU, Renode, Wokwi¶
QEMU — émulation ARM générique¶
QEMU supporte l'émulation de nombreuses cibles ARM (Cortex-M3, M4, A9...) et fait tourner le firmware sans matériel physique. Utile pour les tests d'intégration et le développement quand la cible n'est pas disponible.
# Démarrer QEMU avec une cible STM32 (via plugin)
qemu-system-arm \
-machine lm3s6965evb \
-cpu cortex-m3 \
-nographic \
-kernel firmware.elf \
-semihosting-config enable=on,target=native
Renode — simulation multi-MCU¶
Renode (Antmicro) simule des systèmes multi-MCU complets avec leurs périphériques, y compris les communications entre nœuds (UART, SPI, radio). C'est la solution la plus avancée pour tester l'ensemble d'un réseau IoT en simulation.
# Script Renode : deux nœuds communicant en UART
mach create "capteur"
machine LoadPlatformDescription @platforms/boards/stm32f4_discovery.repl
sysbus LoadELF @firmware_capteur.elf
mach create "gateway"
machine LoadPlatformDescription @platforms/boards/stm32f7_discovery.repl
sysbus LoadELF @firmware_gateway.elf
# Connecte les UART
connector Connect capteur.usart1 gateway.usart2
Wokwi — simulation ESP32 en ligne¶
Wokwi est un simulateur en ligne orienté ESP32, Arduino et RP2040. Il intègre des composants graphiques (LCD, LED, boutons, capteurs virtuels) et peut être utilisé en CI via son API.
CI pour firmware — GitHub Actions¶
# .github/workflows/firmware-ci.yml
name: Firmware CI
on: [push, pull_request]
jobs:
tests-host:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Installer les dépendances
run: |
sudo apt-get install -y cmake gcc ninja-build
pip install gcovr
- name: Build et tests sur host
run: |
cmake -B build-host -DCMAKE_BUILD_TYPE=Debug \
-DTARGET_PLATFORM=host -DENABLE_COVERAGE=ON
cmake --build build-host
ctest --test-dir build-host --output-on-failure
- name: Rapport de couverture
run: gcovr --xml coverage.xml
build-firmware:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Installer ESP-IDF
uses: espressif/esp-idf-ci-action@v1
with:
esp_idf_version: v5.2
- name: Build firmware
run: idf.py build
- name: Upload artefacts
uses: actions/upload-artifact@v4
with:
name: firmware-${{ github.sha }}
path: build/*.bin
Tableau des niveaux de test¶
| Niveau | Où ça tourne | Quoi ça vérifie | Vitesse | Coût | Réalisme |
|---|---|---|---|---|---|
| Test unitaire host | Machine de dev | Logique applicative, algorithmes | Secondes | Nul | Faible |
| Test unitaire cible | MCU réel | HAL, drivers, timing | Minutes | Probe debug | Moyen |
| Test d'intégration QEMU | Émulateur | Interaction composants | Minutes | Nul | Moyen |
| Test d'intégration Renode | Simulateur | Réseau multi-MCU | Minutes | Nul | Moyen |
| Test HIL | Rack matériel | Comportement système complet | Minutes | Élevé | Très élevé |
| Test système cible | Production | Validation finale, régression | Heures | Très élevé | Maximal |
Ce qu'il faut retenir¶
- Isoler la logique du HAL par des interfaces abstraites est la condition sine qua non pour tester sur host.
- Unity est le standard C pour les tests sur cible ; CppUTest pour C++.
- QEMU et Renode permettent de lancer les tests d'intégration en CI sans matériel physique.
- Le HIL est indispensable pour valider les comportements temps réel et les interactions physiques.
- Intégrer les tests dans GitHub Actions dès le début du projet — pas après la phase de développement.
Chapitre suivant : Build, release et OTA — versioning, signature et déploiement du firmware en production.