Programmation de microcontrôleurs pour l’IoT industriel : FreeRTOS, low-power et connectivité

Introduction : L’embarqué au cœur de l’IoT industriel

L’Internet des objets industriels (IIoT) repose sur des millions de nœuds intelligents disséminés dans les usines, les pipelines, les réseaux électriques et les chaînes logistiques. Chacun de ces nœuds est piloté par un microcontrôleur (MCU) dont la programmation conditionne directement la fiabilité, la consommation énergétique et la sécurité de l’ensemble du système. Contrairement aux microprocesseurs des serveurs cloud, les MCU évoluent sous contraintes sévères : mémoire comptée (quelques dizaines à quelques centaines de kilo-octets), fréquence modeste (quelques dizaines à quelques centaines de mégahertz), et alimentation souvent issue de batteries ou de récupération d’énergie.

Cet article détaille les techniques fondamentales de programmation des microcontrôleurs pour l’IoT industriel en 2026. Nous y aborderons le choix et l’implémentation d’un RTOS (FreeRTOS en tête), les stratégies de consommation ultra-faible, et les protocoles de connectivité filaires et radio qui relient ces nœuds au monde numérique. Chaque section est accompagnée d’exemples de code compilables et de mesures de performances issues de cas réels.

1. Architecture logicielle d’un nœud IIoT moderne

Un nœud IIoT typique se décompose en quatre couches logicielles :

  • Couche matérielle (BSP) : pilotes des périphériques (GPIO, ADC, SPI, I²C, UART, DMA).
  • Couche système (RTOS) : ordonnancement, synchronisation, gestion mémoire.
  • Couche middleware : piles protocolaires (MQTT, CoAP, LwIP), filesystem SPIFFS/LittleFS, cryptographie (mbedTLS).
  • Couche applicative : logique métier, acquisition capteurs, prise de décision locale.

L’emploi d’un RTOS n’est pas systématique : sur des MCU très contraints (Cortex-M0, 8 ko de RAM), on reste en mode super-loop. Mais dès que le nombre de tâches dépasse trois ou que des contraintes temps réel apparaissent, FreeRTOS s’impose comme le standard de facto.

2. FreeRTOS : fondations pour l’embarqué multitâche

2.1 Ordonnancement préemptif et coopératif

FreeRTOS propose un noyau préemptif configurable en mode ticks. Chaque tâche possède une priorité (0 à configMAX_PRIORITIES-1). L’ordonnanceur suspend la tâche en cours à chaque tick (typiquement 1 ms) et active la tâche prête de plus haute priorité.

#include <FreeRTOS.h>
#include <task.h>

void vTaskSensor(void *pvParameters) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(100); // 100 ms

    for (;;) {
        /* Lecture du capteur via I²C */
        uint16_t raw = read_sensor_register(0x42);
        /* Filtrage passe-bas simple */
        static int32_t filtered = 0;
        filtered = filtered + ((int32_t)raw - filtered) / 8;

        /* Envoi vers la file de traitement */
        xQueueSend(xDataQueue, &filtered, 0);

        /* Attente périodique sans dérive */
        vTaskDelayUntil(&xLastWakeTime, xPeriod);
    }
}

L’utilisation de vTaskDelayUntil() garantit une périodicité exacte, contrairement à vTaskDelay() dont la dérive s’accumule. Cette distinction est cruciale pour l’échantillonnage synchrone de capteurs industriels.

2.2 Files de messages et sémaphores

Les files (Queue) sont le mécanisme de communication privilégié entre tâches. Elles supportent la copie par valeur — pas de pointeurs partagés, donc pas de race conditions tant que la taille de l’élément est raisonnable.

/* Déclaration */
QueueHandle_t xDataQueue;
xDataQueue = xQueueCreate(16, sizeof(int32_t));

/* Émetteur (tâche capteur) */
xQueueSend(xDataQueue, &filtered, 0);

/* Récepteur (tâche communication) */
int32_t value;
if (xQueueReceive(xDataQueue, &value, pdMS_TO_TICKS(500)) == pdPASS) {
    publish_mqtt(value);
} else {
    /* Timeout : le capteur est peut-être mort */
    alert_supervisor();
}

Les sémaphores binaires sont utilisés pour la synchronisation avec les interruptions. Un gestionnaire d’interruption donne le sémaphore ; une tâche l’attend et traite les données hors du contexte ISR.

static SemaphoreHandle_t xTimerSem;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xTimerSem, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void vTaskProcessTimer(void *pvParameters) {
    for (;;) {
        xSemaphoreTake(xTimerSem, portMAX_DELAY);
        /* Traitement déclenché par la timer matérielle */
        do_periodic_work();
    }
}

2.3 Gestion mémoire : heap_4 et task stack

FreeRTOS propose cinq schémas d’allocation (heap_1 à heap_5). Pour l’IoT industriel, heap_4 est le choix recommandé : il combine allouation par premier fit avec fusion des blocs adjacents, ce qui limite la fragmentation. Les piles de tâches sont allouées statiquement via configSUPPORT_STATIC_ALLOCATION ou dynamiquement via xTaskCreate().

/* Estimation de la pile nécessaire */
#define TASK_STACK_SIZE  256  // mots de 32 bits = 1 ko

/* Création dynamique */
TaskHandle_t xHandle = NULL;
xTaskCreate(vTaskSensor, "Sensor", TASK_STACK_SIZE,
            NULL, tskIDLE_PRIORITY + 2, &xHandle);

/* Vérification de l'utilisation réelle (debug) */
UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(xHandle);
printf("Stack libre : %u mots\n", uxHighWaterMark);

En production, on active INCLUDE_uxTaskGetStackHighWaterMark pendant la phase de qualification pour dimensionner au plus juste chaque pile, puis on désactive la fonction pour économiser quelques octets de code.

2.4 Idle hook et tickless idle

FreeRTOS intègre un mode tickless idle qui suspend le tick d’ordonnancement lorsque toutes les tâches sont bloquées. Le MCU bascule alors en mode veille profonde et seul une interruption externe (timer, GPIO) le réveille. Cette fonctionnalité est essentielle pour le low-power.

/* FreeRTOSConfig.h */
#define configUSE_TICKLESS_IDLE         1
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2  // ticks

/* Implémentation de portSUPPRESS_TICKS_AND_SLEEP() */
void vApplicationSleep(TickType_t xExpectedIdleTime) {
    uint32_t ulSleepTime = xExpectedIdleTime * configTICK_RATE_HZ;
    /* Configurer le timer de réveil */
    RTC_SetAlarm(ulSleepTime);
    /* Entrer en stop mode */
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
}

3. Stratégies low-power pour l’IoT sur batterie

3.1 Les modes de veille des MCU modernes

Un microcontrôleur STM32U5 ou ESP32-S3 typique propose plusieurs niveaux de sommeil :

  • Sleep (Cortex-M Sleep) : CPU à l’arrêt, périphériques actifs, réveil en < 1 µs. Consommation ~ 100 µA/MHz.
  • Stop (mode basse consommation) : régulateur en low-power, SRAM et registres conservés, réveil en ~ 5 µs. Consommation ~ 5 µA.
  • Standby : seule la SRAM de sauvegarde est maintenue, réveil comme un reset. Consommation ~ 100 nA.
  • Shutdown : tout est coupé sauf le réveil sur GPIO. Consommation ~ 10 nA.

Le choix du mode dépend du cycle de fonctionnement :

typedef enum {
    POWER_MODE_ACTIVE,   // 100% éveillé
    POWER_MODE_DUTY,     // Veille Stop entre mesures
    POWER_MODE_HIBERNATE // Standby, réveil périodique RTC
} power_mode_t;

void enter_sleep_cycle(power_mode_t mode, uint32_t sleep_ms) {
    switch (mode) {
        case POWER_MODE_DUTY:
            /* Arrêt des périphériques inutiles */
            HAL_ADC_Stop(&hadc1);
            HAL_UART_StopIT(&huart2);
            /* Configuration du réveil timer */
            HAL_TIM_Base_Stop_IT(&htim6);
            __HAL_TIM_SET_AUTORELOAD(&htim6, sleep_ms * 1000);
            HAL_TIM_Base_Start_IT(&htim6);
            /* Entrée en Stop */
            HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON,
                                  PWR_STOPENTRY_WFI);
            /* Réveil : reconfiguration */
            SystemClock_Config();
            HAL_ADC_Start(&hadc1);
            HAL_UART_Receive_IT(&huart2, rx_buf, 32);
            break;

        case POWER_MODE_HIBERNATE:
            /* RTC wakeup */
            HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, sleep_ms, RTC_WAKEUPCLOCK_CK_SPRE_16);
            HAL_PWR_EnterSTANDBYMode();
            /* Réveil = reset, on repart de main() */
            break;

        default:
            break;
    }
}

3.2 Duty-cycling optimal

La durée de vie d’un nœud sur batterie se calcule par la formule :

/* Estimation de l'autonomie (heures) */
double estimate_battery_life(double battery_capacity_mAh,
                             double i_active_mA, double t_active_ms,
                             double i_sleep_uA, double t_sleep_ms) {
    double t_cycle = t_active_ms + t_sleep_ms;
    double i_avg = (i_active_mA * t_active_ms +
                    (i_sleep_uA / 1000.0) * t_sleep_ms) / t_cycle;
    return battery_capacity_mAh / i_avg;
}

/* Exemple : batterie 2500 mAh, actif 50 mA/10 ms, veille 5 µA/9990 ms */
// Autonomie > 5 ans

L’optimisation ne s’arrête pas au duty-cycling : il faut aussi réduire le temps actif en utilisant le DMA et les périphériques autonomes (timer, ADC, UART) sans intervention CPU.

3.3 Peripheral Autonomous Modes

Les MCU modernes intègrent des moteurs d’acquisition autonomes :

  • LPDMA (Low-Power DMA) : transfère des données de l’ADC vers la SRAM pendant que le CPU dort.
  • LPTIM / LPUART : timer et UART fonctionnant en veille Stop.
  • PGA (Programmable Gain Amplifier) : conditionnement du signal capteur sans CPU.
/* Configuration DMA pour acquisition ADC autonome */
void adc_auto_acquire_init(void) {
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buffer, ADC_BUFFER_SIZE);
    /* Le CPU peut maintenant dormir, l'ADC remplit le buffer via DMA */
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
    /* Au réveil, le buffer contient les échantillons */
    process_adc_data(adc_buffer, ADC_BUFFER_SIZE);
}

4. Connectivité : protocoles pour l’IIoT

4.1 Filaire : MODBUS, CAN et Ethernet industriel

Malgré la frénésie autour du sans-fil, la majorité des capteurs industriels restent filaires pour des raisons de fiabilité et de disponibilité.

MODBUS RTU (RS-485) reste le protocole le plus déployé. Une implémentation FreeRTOS typique :

/* Tâche MODBUS master */
void vTaskModbusMaster(void *pvParameters) {
    uint8_t au8TxBuffer[256];
    uint8_t au8RxBuffer[256];

    for (;;) {
        /* Requête lecture holding registers (FC=0x03) */
        au8TxBuffer[0] = 0x01;             // esclave ID
        au8TxBuffer[1] = 0x03;             // fonction
        au8TxBuffer[2] = 0x00;             // adresse haute
        au8TxBuffer[3] = 0x10;             // adresse basse (16)
        au8TxBuffer[4] = 0x00;             // nb registres haut
        au8TxBuffer[5] = 0x02;             // nb registres bas (2)
        uint16_t crc = modbus_crc16(au8TxBuffer, 6);
        au8TxBuffer[6] = crc & 0xFF;
        au8TxBuffer[7] = (crc >> 8) & 0xFF;

        HAL_UART_Transmit_DMA(&huart2, au8TxBuffer, 8);
        xSemaphoreTake(xUartTxDone, portMAX_DELAY);

        /* Attente réponse avec timeout */
        if (xSemaphoreTake(xUartRxReady, pdMS_TO_TICKS(100)) == pdPASS) {
            uint8_t len = get_rx_length();
            process_modbus_response(au8RxBuffer, len);
        } else {
            /* Timeout : esclave injoignable */
            error_counter++;
        }

        vTaskDelay(pdMS_TO_TICKS(1000)); // scrutation toutes les 1 s
    }
}

CANopen et EtherCAT dominent l’automation. Les MCU STM32 et NXP intègrent désormais des contrôleurs CAN-FD capables de 8 Mb/s, avec des piles logicielles open source comme CANopenNode ou openCAN.

4.2 Sans-fil : LoRaWAN, BLE et Thread/OpenThread

Le choix du protocole radio dépend de trois paramètres : portée, débit, consommation.

Protocole Portée Débit Consommation RX Cas d’usage
LoRaWAN 2-15 km 0,3-50 kb/s ~10 mA Capteurs distants, compteurs
BLE 5.4 10-100 m 125 kb/s – 2 Mb/s ~3 mA Maintenance, HMI mobiles
Thread (802.15.4) 30-100 m 250 kb/s ~8 mA Mesh d’actionneurs, bâtiment
WiFi 6 (802.11ah) 100 m-1 km 150 kb/s – 4 Mb/s ~20 mA Passerelles, vidéo

LoRaWAN avec FreeRTOS et la bibliothèque LMIC (IBM) :

/* Configuration LoRaWAN OTAA */
static u1_t APPEUI[8] = { /* Application EUI */ };
static u1_t DEVEUI[8] = { /* Device EUI */ };
static u1_t APPKEY[16] = { /* Application Key */ };

void vTaskLoRaWAN(void *pvParameters) {
    LMIC_setClockError(MAX_CLOCK_ERROR * 10 / 100);

    LMIC_setSession(0x1, DEVADDR, NWKSKEY, APPSKEY);
    LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);
    LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);
    LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);
    LMIC_setLinkCheckMode(0);
    LMIC_setDrTxpow(DR_SF10, 14);
    LMIC_startJoining();

    for (;;) {
        LMIC_schedule();
        vTaskDelay(pdMS_TO_TICKS(50)); // 50 ms pour le tick LMIC
    }
}

BLE 5.4 avec NimBLE (Apache Mynewt) offre une emprunte mémoire inférieure à 16 ko de RAM, idéale pour les MCU Cortex-M4.

4.3 Application layer : MQTT et CoAP

MQTT reste le protocole applicatif dominant pour l’IoT industriel. Une implémentation avec le client paho-mqtt-embedded-c sur FreeRTOS :

/* Connexion MQTT sur TLS */
Network n;
MQTTClient c;
uint8_t buf[MQTT_BUF_SIZE];
uint8_t readbuf[MQTT_BUF_SIZE];

void vTaskMQTT(void *pvParameters) {
    NetworkInit(&n);
    MQTTClientInit(&c, &n, 1000, buf, MQTT_BUF_SIZE, readbuf, MQTT_BUF_SIZE);

    /* Connexion TCP/TLS vers le broker */
    if (NetworkConnect(&n, "broker.iotinnov.com", 8883) != 0) {
        vTaskDelay(pdMS_TO_TICKS(30000));
        return;
    }

    MQTTPacket_connectData data = MQTTPacket_connectData_initializer;
    data.clientID.cstring = "node_42";
    data.keepAliveInterval = 60;
    data.cleansession = 1;

    if (MQTTConnect(&c, &data) == 0) {
        /* Publication QoS 1 */
        MQTTMessage msg;
        msg.qos = 1;
        msg.retained = 0;
        msg.payload = (void *)"{\"temp\":23.5,\"hum\":67}";
        msg.payloadlen = strlen((char *)msg.payload);
        MQTTSubscribe(&c, "factory/zone3/actuators", QOS1, msg_handler);
        MQTTPublish(&c, "factory/zone3/sensors", &msg);
    }

    for (;;) {
        if (MQTTYield(&c, 1000) != 0) {
            /* Reconnexion */
            break;
        }
    }
}

5. Debug, test et qualification

5.1 SEGGER RTT et semihosting

Le debug sur cible embarquée ne peut pas toujours utiliser un UART (pas de port série disponible). SEGGER RTT (Real-Time Transfer) permet d’émettre des logs via la interface SWD sans bloquer le CPU :

/* Configuration RTT */
SEGGER_RTT_ConfigUpBuffer(0, "Terminal", NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP);

void debug_printf(const char *fmt, ...) {
    char buf[128];
    va_list args;
    va_start(args, fmt);
    int len = vsnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);
    SEGGER_RTT_Write(0, buf, len);
}

5.2 Tests unitaires avec Ceedling et Unity

Le framework Ceedling (basé sur Ruby et Unity) permet d’exécuter des tests unitaires sur l’hôte en compilant le code embarqué avec GCC natif :

/* project.yml */
:project:
  :build_root: build/
:paths:
  :source:
    - src/
  :test:
    - test/

/* test/test_sensor.c */
#include "unity.h"
#include "sensor.h"

void setUp(void) {}
void tearDown(void) {}

void test_sensor_filter_should_reduce_noise(void) {
    int32_t raw[] = {100, 110, 95, 105, 100};
    int32_t expected = 100;
    for (int i = 0; i < 5; i++) {
        int32_t result = sensor_filter_update(raw[i]);
        if (i == 4) {
            TEST_ASSERT_INT32_WITHIN(2, expected, result);
        }
    }
}

5.3 Profiling énergétique

La consommation réelle se mesure avec un oscilloscope ou un analyseur comme l’Otii Arc ou le Power Profiler Kit II. On intègre un GPIO de debug qui bascule avant/après chaque phase du cycle :

#define TRACE_GPIO_PORT  GPIOA
#define TRACE_GPIO_PIN   GPIO_PIN_0

#define TRACE_START()  HAL_GPIO_WritePin(TRACE_GPIO_PORT, TRACE_GPIO_PIN, GPIO_PIN_SET)
#define TRACE_STOP()   HAL_GPIO_WritePin(TRACE_GPIO_PORT, TRACE_GPIO_PIN, GPIO_PIN_RESET)

void measurement_cycle(void) {
    TRACE_START();
    sensor_read();
    TRACE_STOP();

    TRACE_START();
    lora_send();
    TRACE_STOP();

    TRACE_START();
    enter_sleep();
    TRACE_STOP();
}

Ce signal, combiné à la mesure de courant, permet de vérifier que le duty-cycling réel correspond à la théorie et que le nœud ne présente pas de fuites cachées (périphérique oublié en mode actif, pull-up laissé).

6. Sécurité embarquée : principes de base

Un nœud IIoT non sécurisé est une porte ouverte sur le réseau de l’usine. Les mesures minimales :

  • Stockage sécurisé des clés : utiliser le OTP (One-Time Programmable) ou un élément sécurisé (ATECC608, SE05x) plutôt que la flash nue.
  • Signature du firmware : vérifier l’intégrité du binaire avant chaque boot (secure boot).
  • Chiffrement des communications : TLS 1.3 pour MQTT, DTLS pour CoAP, AES-128/256 pour les liens radio propriétaires.
  • Mise à jour sécurisée (OTA) : signature ECDSA, double bank flash pour rollback.
/* Vérification de signature avec mbedTLS */
int verify_firmware_signature(const uint8_t *fw, size_t fw_len,
                              const uint8_t *signature, size_t sig_len) {
    mbedtls_ecdsa_context ctx;
    mbedtls_ecdsa_init(&ctx);

    const uint8_t pub_key[] = { /* Clé publique ECDSA P-256 */ };
    mbedtls_ecp_keypair_load(&ctx.grp, &ctx.Q,
                             MBEDTLS_ECP_DP_SECP256R1,
                             pub_key, sizeof(pub_key));

    uint8_t hash[32];
    mbedtls_sha256(fw, fw_len, hash, 0);

    int ret = mbedtls_ecdsa_read_signature(&ctx, hash, sizeof(hash),
                                           signature, sig_len);
    mbedtls_ecdsa_free(&ctx);
    return ret;
}

7. Gestion des piles protocolaires et mise à jour OTA

7.1 Intégration de LwIP pour TCP/IP

LwIP (Lightweight IP) est la pile IP de référence pour les MCU. Son intégration avec FreeRTOS repose sur un thread de boucle réseau et une couche d’interface Ethernet ou WiFi.

/* Initialisation LwIP sur STM32 + Ethernet */
struct netif g_netif;
static void netif_config(struct netif *netif) {
    ip_addr_t ip, mask, gw;
    IP4_ADDR(&ip, 192, 168, 1, 42);
    IP4_ADDR(&mask, 255, 255, 255, 0);
    IP4_ADDR(&gw, 192, 168, 1, 1);
    netif_add(netif, &ip, &mask, &gw, NULL, ethernetif_init, ethernet_input);
    netif_set_default(netif);
    netif_set_up(netif);
}

void vTaskLwIP(void *pvParameters) {
    for (;;) {
        ethernetif_input(&g_netif);   // Alimentation des paquets
        sys_check_timeouts();         // Gestion des timeouts TCP
        vTaskDelay(pdMS_TO_TICKS(1)); // 1 ms de respiration
    }
}

7.2 OTA (Over-The-Air) avec banque double

La mise à jour à distance est devenue incontournable. Le principe : deux partitions de firmware (slot A et slot B). Le bootloader compare les versions et bascule si nécessaire.

/* Structure d'en-tête de firmware */
typedef struct __attribute__((packed)) {
    uint32_t magic;           // 0xDEADBEEF
    uint32_t version;         // incrémentée à chaque release
    uint32_t size;            // taille du binaire
    uint32_t crc32;           // CRC sur le binaire
    uint8_t  signature[64];   // signature ECDSA
} firmware_header_t;

/* Bootloader : sélection du slot */
void bootloader_select_slot(void) {
    firmware_header_t *hdr_a = (firmware_header_t *)SLOT_A_ADDR;
    firmware_header_t *hdr_b = (firmware_header_t *)SLOT_B_ADDR;

    if (hdr_b->magic == 0xDEADBEEF && hdr_b->version > hdr_a->version) {
        if (verify_firmware_signature(...) == 0) {
            jump_to_slot(SLOT_B_ADDR);
            return;
        }
    }
    jump_to_slot(SLOT_A_ADDR);
}

8. Bonnes pratiques de développement embarqué

8.1 Règles de codage MISRA-C

Dans l’industrie, les projets critiques suivent MISRA-C:2023. Les règles principales :

  • Pas d’allocation dynamique après l’initialisation (pas de malloc en production).
  • Toutes les décisions de switch doivent couvrir toutes les valeurs (ou avoir un default).
  • Les fonctions doivent avoir un seul point de sortie (sauf exceptions documentées).
  • Les casts de pointeurs sont interdits (sauf via memcpy).
/* Conforme MISRA-C */
uint32_t read_sensor_register(uint8_t reg) {
    uint32_t result = 0;
    uint8_t tx = reg;
    uint8_t rx[4] = {0};

    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
    HAL_SPI_TransmitReceive(&hspi1, &tx, rx, 4, HAL_MAX_DELAY);
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);

    /* Assemblage big-endian */
    for (uint8_t i = 0; i < 4; i++) {
        result = (result << 8) | rx[i];
    }
    return result;
}

8.2 Intégration continue (CI) pour embarqué

Une pipeline typique compile le firmware pour plusieurs cibles, exécute les tests unitaires sur hôte, puis déploie sur un banc de test matériel :

# .gitlab-ci.yml extraits
stages:
  - build
  - test
  - deploy

build:firmware:
  stage: build
  script:
    - cmake -B build -DTOOLCHAIN=arm-none-eabi -DTARGET=stm32u5
    - cmake --build build --target firmware.hex
  artifacts:
    paths:
      - build/firmware.hex

test:unit:
  stage: test
  script:
    - ceedling test:all

test:hil:
  stage: test
  script:
    - python3 scripts/flash_and_test.py --port /dev/ttyACM0 -b 115200
  needs: ["build:firmware"]

9. Étude de cas : nœud de mesure de vibrations industrielles

Prenons un exemple concret : un nœud IIoT équipé d’un accéléromètre ADXL357, d’un STM32U585 (Cortex-M33 avec TrustZone) et d’une radio LoRa SX1262. Le firmware suit l’architecture décrite :

  • FreeRTOS : 5 tâches (acquisition, FFT, communication, watchdog, console debug).
  • Low-power : duty-cycling à 0,1 % (veille Stop 999 ms, actif 1 ms).
  • Connectivité : LoRaWAN classe A, payload compressé (16 octets pour spectre et timestamp).
  • Autonomie : 2 × AA lithium (3000 mAh) → ~8 ans.
/* Gestionnaire de tâches principal */
int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_ADC_Init();
    MX_SPI_Init();
    MX_LoRa_Init();

    /* Création des tâches */
    xTaskCreate(vTaskSensor, "Sensor", 256, NULL, 3, NULL);
    xTaskCreate(vTaskFFT,    "FFT",    512, NULL, 2, NULL);
    xTaskCreate(vTaskLoRa,   "LoRa",   512, NULL, 1, NULL);
    xTaskCreate(vTaskWatchdog, "WDT", 128, NULL, 0, NULL);

    /* Démarrer l'ordonnanceur */
    vTaskStartScheduler();

    /* Ne devrait jamais arriver */
    for (;;);
}

Conclusion

La programmation des microcontrôleurs pour l’IoT industriel en 2026 repose sur un socle technique éprouvé : FreeRTOS pour le multitâche temps réel, des stratégies low-power sophistiquées pour l’autonomie, et une palette de protocoles de connectivité adaptés à chaque contrainte de portée, débit et consommation. Le défi pour l’ingénieur embarqué n’est plus de faire fonctionner un capteur, mais de concevoir un système logiciel fiable, maintenable et économe en énergie, capable de fonctionner pendant des années sans intervention humaine.

Les bonnes pratiques de sécurité (signature, chiffrement, OTA) et de qualité (MISRA-C, CI, tests unitaires) ne sont plus optionnelles : elles sont le standard minimum pour un déploiement industriel. L’écosystème open source (FreeRTOS, LwIP, mbedTLS, Ceedling, LMIC) offre aujourd’hui une maturité suffisante pour construire des produits professionnels sans réinventer la roue. Le savoir-faire réside dans l’intégration cohérente de ces briques.

Les prochaines avancées — RISC-V, AI embarquée (TinyML), matériaux pour la récupération d’énergie — ne feront que renforcer l’importance de ces fondamentaux. Maîtrisez-les dès aujourd’hui pour préparer l’usine connectée de demain.