Aller au contenu

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.