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
mallocen 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.
