Le développement embarqué n'est pas un domaine figé. La complexité monte, les contraintes de sécurité se durcissent, les outils évoluent — parfois rapidement. On ne parle plus de bricoler un firmware sur un PIC en assembleur dans le fond d'un garage.
Il existe deux grandes familles de systèmes, avec des profils très différents : les microcontrôleurs sous RTOS (Cortex-M, RISC-V 32 bits, AVR…) et les processeurs applicatifs sous Linux embarqué (Cortex-A, x86, RISC-V 64 bits). Les deux ont leurs règles, leurs pièges, leurs outils. Et ils se rapprochent — lentement, mais sûrement.
Voilà ce qu'on a retenu de l'expérience terrain.
Partie 1 — RTOS et microcontrôleurs
Travailler sans filet
Un microcontrôleur Cortex-M0, c'est 32 kB de Flash, peut-être 8 kB de RAM, pas de MMU, pas d'OS au sens traditionnel du terme. Il tourne à 48 MHz et consomme quelques milliampères. Ce n'est pas une limitation qu'on subit — c'est la contrainte qui définit le produit. La pile d'un capteur IoT qui doit tenir 10 ans, ça se dimensionne au bit près.
Quelques réalités du quotidien dans ce monde :
- Pas de MMU : un pointeur fou écrase la pile sans prévenir. Les outils de détection (MPU sur Cortex-M3+) existent mais ne couvrent pas tout.
- RAM comptée en kB : chaque allocation est réfléchie. Les allocations dynamiques en heap sont souvent prohibées en prod — trop de risques de fragmentation.
- Déterminisme : la tâche de contrôle moteur doit se déclencher dans un délai garanti. C'est la raison d'être du temps-réel.
- Accès direct aux registres : on parle au hardware sans abstraction OS. Un seul octet mal écrit dans un registre de configuration et le périphérique est dans un état indéfini.
- Débogage difficile :
printfsur UART reste le premier outil utilisé dans 80 % des cas. GDB via SWD/JTAG fonctionne, mais on perd vite le contexte dès qu'on touche aux interruptions.
Les RTOS — tour d'horizon
Un RTOS (Real-Time Operating System) apporte l'ordonnancement préemptif, la gestion des tâches, les primitives de synchronisation (mutex, sémaphores, files de messages), et parfois une couche réseau et un système de fichiers. Plusieurs acteurs se partagent le marché.
FreeRTOS
Le plus déployéUltra-léger : 4 Ko RAM minimum, support sur à peu près tout ce qui embarque un MCU. C'est le premier nom qui vient quand on dit "RTOS".
Points forts : ubiquité, docs abondantes, BSP vendeur souvent livré clé en main, MIT license.
Limites : pas de HAL unifié, pas de Devicetree, configuration via FreeRTOSConfig.h qui grossit et devient vite difficile à maintenir. Acquis par Amazon en 2017 (AWS FreeRTOS / coreMQTT, etc.) — le focus cloud est réel.
NuttX
POSIX sur MCUUn pari audacieux : implémenter POSIX complet sur microcontrôleur. open(), read(), threads POSIX, sockets BSD — tout y est.
Points forts : portabilité du code applicatif, utilisé par SpaceX sur Dragon, architecture propre et bien pensée.
Limites : intégration complexe, courbe d'apprentissage élevée, communauté nettement plus petite. On ne trouve pas facilement quelqu'un qui le connaît bien.
RIOT
IoT basse consoFocus IoT basse consommation, excellent support des protocoles radio : IEEE 802.15.4, LoRa, BLE, 6LoWPAN. Conçu pour tourner sur des nœuds contraints.
Points forts : pile réseau IoT bien intégrée, communauté académique européenne active, approche modulaire.
Limites : support industriel plus limité, BSP vendeur souvent absent — on fait une partie du travail soi-même.
Zephyr
Standard émergentLinux Foundation, lancé en 2016. C'est là que le secteur va. Architecture calquée sur Linux, Devicetree, support commercial exceptionnel.
Points forts : Nordic, ST, TI, Espressif, NXP, Renesas maintiennent tous en upstream. Écosystème qui grandit vite, documentation sérieuse.
Limites : plus lourd que FreeRTOS (20–50 Ko RAM selon la config), temps de build plus long, courbe d'apprentissage non nulle. On y revient en détail.
Tableau comparatif
| RTOS | RAM min | Intégration | Écosystème | Temps-réel | Support indus. | Devicetree |
|---|---|---|---|---|---|---|
| FreeRTOS | ~4 Ko | Simple | Très riche | Oui | Excellent | Non |
| NuttX | ~32 Ko | Complexe | Moyen | Oui | Limité | Non |
| RIOT | ~1,5 Ko | Modéré | Correct | Oui | Faible | Non |
| Zephyr | ~20 Ko | Modéré | Excellent | Oui | Excellent | Oui |
Pourquoi Zephyr s'impose comme RTOS de référence
Quatre raisons concrètes, pas des arguments marketing.
-
1
Devicetree — le hardware est déclaratif, pas codé en dur. Vous décrivez votre circuit dans un fichier
.dts, le firmware s'adapte à la compilation. Changer de board, ajouter un capteur — ça devient une modification de fichier texte, pas une chasse au#definedans dix fichiers. Et les ingénieurs qui viennent du monde Linux sont immédiatement à l'aise.
Un overlay typique pour un capteur I2C :
/* boards/my_board.overlay */
&i2c0 {
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>; /* 400 kHz */
bme280: bme280@76 {
compatible = "bosch,bme280";
reg = <0x76>;
label = "BME280";
};
};
/* Dans le code C, on récupère le device proprement : */
/* const struct device *dev =
DEVICE_DT_GET(DT_NODELABEL(bme280)); */
-
2
CMake + Kconfig — le même système que le kernel Linux. Les options de build se configurent dans un
prj.conflisible par un humain. Pas de GUI de configuration ésotérique, pas de Makefile à rallonge.
# Activer le Bluetooth LE
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="Codium Sensor"
# Stack réseau + TLS
CONFIG_NET_SOCKETS=y
CONFIG_MBEDTLS=y
# Logging
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3 # INFO
# MPU pour la détection d'accès mémoire invalides
CONFIG_ARM_MPU=y
-
3
Support hardware en upstream. Nordic Semiconductor, STMicroelectronics, Texas Instruments, Espressif (ESP32), Microchip, NXP, Renesas — ils maintiennent tous leurs BSP directement dans le dépôt principal Zephyr. Ce n'est pas une contribution symbolique : c'est du code testé en CI à chaque commit, avec des runners hardware réels pour certains vendeurs. Quand vous ciblez un nRF9160 ou un STM32U5, vous avez un point de départ solide, pas un fork vieux de trois ans.
-
4
west — le gestionnaire de workspace multi-dépôts. Un manifeste YAML décrit toutes vos dépendances (Zephyr lui-même, des modules tiers, votre propre code). Reproductibilité garantie entre développeurs et en CI.
manifest:
defaults:
remote: upstream
remotes:
- name: upstream
url-base: https://github.com/zephyrproject-rtos
- name: codium
url-base: https://github.com/codium-electronique
projects:
- name: zephyr
remote: upstream
revision: v3.7.0
import: true
- name: firmware-drivers
remote: codium
path: modules/codium-drivers
revision: main
Partie 2 — Linux embarqué
Changer d'échelle — mais les contraintes restent
On passe à un processeur applicatif : Cortex-A53, i.MX8, RK3568, ou x86 Atom. MMU, cache L1/L2, plusieurs cœurs, RAM en centaines de Mo voire en Go. Linux tourne dessus, avec son écosystème complet.
Mais "Linux" ne veut pas dire "facile". Quelques réalités :
- Démarrage < 10 s : dans beaucoup de produits industriels, c'est une exigence dure. Le kernel prend du temps. Il faut optimiser l'initrd, désactiver les subsystems inutiles, parfois patcher le bootloader.
- Temps-réel : le kernel standard n'est pas temps-réel. PREEMPT_RT (désormais intégré en mainline depuis 6.12) réduit drastiquement la latence maximale — on descend sous 100 µs sur un Cortex-A53 bien configuré. Suffisant pour du contrôle motion moyen terme, pas pour de la commande à la µs.
- Certifications : IEC 62443 pour l'industrie, IEC 61508 pour la sécurité fonctionnelle — Linux mainstream n'y est pas certifiable tel quel. Des variants comme RTEMS ou des configurations Linux très réduites et validées existent pour ces cas.
- Les règles du kernel : pas d'ABI interne stable, pas d'allocation mémoire dans un contexte d'interruption (
GFP_ATOMICseulement), revue de code exigeante pour tout ce qui va en upstream. Si vous écrivez un driver maison, vous êtes seul à le maintenir — c'est un coût réel.
"Un driver Linux out-of-tree, c'est de la dette technique dès le premier commit. Chaque nouvelle version du kernel peut casser votre ABI interne. Aller en upstream, c'est du travail — mais c'est un investissement."
Retour d'expérience CodiumStructure d'un driver Linux
Le kernel Linux organise les drivers autour du Device Model : chaque driver est lié à un bus (I2C, SPI, platform, PCI…), s'enregistre auprès d'un sous-système fonctionnel (IIO pour les capteurs, input pour les périphériques d'entrée, DRM pour l'affichage, V4L2 pour la vidéo…), et expose une interface standard vers l'espace utilisateur.
Cette architecture en couches a un avantage concret : le sous-système gère la partie générique (gestion des événements, interface sysfs, buffers) et votre driver ne fait que le strict nécessaire — lire les registres, gérer les interruptions, traduire les données brutes. Ce découplage facilite la maintenance et permet la réutilisation entre produits.
En pratique, un driver commence toujours par un probe() (appel à la détection du device) et un remove() (nettoyage). Entre les deux, vous configurez le hardware, enregistrez les ressources (IRQ, regulator, clock…) via les APIs kernel, et vous raccordez au sous-système fonctionnel.
BSP custom sur base Debian/Fedora
Quand on porte Linux sur une carte custom, on construit un BSP (Board Support Package) — l'ensemble kernel + bootloader + root filesystem adapté au hardware. Les grandes étapes :
-
1
Kernel LTS + patches SoC On part d'un kernel LTS (6.6, 6.12…), on intègre les patches du fabricant SoC (Rockchip, NXP, ST…) — souvent un dépôt git public maintenu par le vendeur. On active uniquement les sous-systèmes nécessaires. Un
.configde production fait typiquement 200–400 options, pas 4000. -
2
Device Tree Source (DTS) custom On part du DTS de référence du SoC, on l'étend avec les périphériques propres à la carte : GPIO, regulateurs d'alimentation, panneaux d'affichage, capteurs. Un overlay propre, versionné avec le schématique.
-
3
U-Boot Le bootloader. Il initialise la RAM, charge le kernel et le DTS en mémoire, passe le contrôle. Configuration via Kconfig, identique au kernel. Quelques scripts d'environnement pour le fallback (mise à jour OTA ratée → rollback automatique).
-
4
Root filesystem On base sur Debian stable (debootstrap) ou Fedora, on épure agressivement, on ajoute les packages métier. Résultat : un système de 300 Mo à 1,2 Go selon les besoins, avec des mises à jour OTA sécurisées (Mender, RAUC, ou custom).
-
5
Packages propriétaires Firmware GPU (blob), codec vidéo, stack de cryptographie certifiée, daemon métier. Intégrés dans le rootfs via des packages Debian signés — reproductibilité et traçabilité garanties.
Réalisation concrète : driver écran MIPI DSI + contrôleur tactile I2C
Un exemple concret et récurrent dans nos projets : une carte custom avec un écran MIPI DSI et un contrôleur tactile I2C. Deux drivers distincts, deux sous-systèmes kernel, un lien dans le Device Tree.
Le panel MIPI DSI — sous-système DRM
Un panel DRM implémente quelques callbacks : prepare() met le panel sous tension, enable() sort du reset et envoie les commandes d'initialisation DCS. À l'inverse, disable() et unprepare() gèrent l'extinction propre.
static int my_panel_prepare(struct drm_panel *panel)
{
struct my_panel *ctx = to_my_panel(panel);
/* Séquence power-on : regulator puis GPIO reset */
regulator_enable(ctx->vdd);
usleep_range(5000, 6000);
gpiod_set_value_cansleep(ctx->reset_gpio, 1);
usleep_range(10000, 12000);
gpiod_set_value_cansleep(ctx->reset_gpio, 0);
msleep(120); /* Attente spécifiée par le datasheet */
return 0;
}
static int my_panel_enable(struct drm_panel *panel)
{
struct my_panel *ctx = to_my_panel(panel);
struct mipi_dsi_device *dsi = ctx->dsi;
/* Séquence d'init DCS — sortie du sleep, allumage */
mipi_dsi_dcs_exit_sleep_mode(dsi);
msleep(120);
mipi_dsi_dcs_set_display_on(dsi);
return 0;
}
static const struct drm_panel_funcs my_panel_funcs = {
.prepare = my_panel_prepare,
.enable = my_panel_enable,
.disable = my_panel_disable,
.unprepare = my_panel_unprepare,
.get_modes = my_panel_get_modes,
};
Le contrôleur tactile — sous-système input
Le touch controller envoie une interruption à chaque contact détecté. Le driver lit les coordonnées via I2C et rapporte les événements au sous-système input en utilisant l'API multi-touch (type B).
static irqreturn_t my_ts_irq_handler(int irq, void *data)
{
struct my_ts *ts = data;
struct touch_point points[MY_TS_MAX_FINGERS];
int count, i;
count = my_ts_read_touch_data(ts->client, points);
if (count < 0)
return IRQ_HANDLED;
for (i = 0; i < MY_TS_MAX_FINGERS; i++) {
input_mt_slot(ts->input, i);
if (i < count) {
input_mt_report_slot_state(ts->input,
MT_TOOL_FINGER, true);
input_report_abs(ts->input, ABS_MT_POSITION_X,
points[i].x);
input_report_abs(ts->input, ABS_MT_POSITION_Y,
points[i].y);
} else {
input_mt_report_slot_state(ts->input,
MT_TOOL_FINGER, false);
}
}
input_mt_report_pointer_emulation(ts->input, true);
input_sync(ts->input);
return IRQ_HANDLED;
}
Le binding Device Tree
Le DTS relie les deux drivers et décrit le hardware : quel GPIO pour le reset du panel, quelle IRQ pour le touch, quels regulateurs.
&mipi_dsi {
status = "okay";
panel@0 {
compatible = "vendor,my-panel";
reg = <0>;
reset-gpios = <&gpio3 RK_PB6 GPIO_ACTIVE_LOW>;
vdd-supply = <&vcc_lcd>;
/* Timing MIPI DSI */
dsi,lanes = <4>;
dsi,format = <MIPI_DSI_FMT_RGB888>;
dsi,flags = <MIPI_DSI_MODE_VIDEO |
MIPI_DSI_MODE_VIDEO_BURST>;
/* Référence au touch controller (pour la liaison DRM) */
touchscreen = <&my_touch>;
};
};
&i2c4 {
status = "okay";
my_touch: touchscreen@38 {
compatible = "vendor,my-touch";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <RK_PA7 IRQ_TYPE_EDGE_FALLING>;
reset-gpios = <&gpio3 RK_PB7 GPIO_ACTIVE_LOW>;
vcc-supply = <&vcc_tp>;
touchscreen-size-x = <1280>;
touchscreen-size-y = <800>;
};
};
compatible, et il cherche un driver enregistré qui correspond. C'est le Device Model en action — découplage total entre description hardware et code driver.
En conclusion
Ces deux mondes restent distincts. Un ingénieur spécialisé microcontrôleurs et un ingénieur Linux embarqué n'ont pas les mêmes réflexes, pas les mêmes outils, pas les mêmes problèmes. Et c'est normal — les contraintes fondamentales sont différentes.
Ce qui change, c'est le rapprochement progressif des outils. Zephyr a délibérément copié l'architecture de Linux : Devicetree, Kconfig, CMake, west. Un ingénieur qui fait du kernel Linux le matin peut comprendre un projet Zephyr l'après-midi sans être complètement perdu. C'est nouveau, et c'est utile.
Les challenges fondamentaux restent. La RAM compte toujours. Le déterminisme ne s'improvise pas. Un pointeur mal géré est toujours un bug silencieux. Une allocation dans un handler d'IRQ, c'est toujours du code faux. Ces règles ne changent pas parce que les outils deviennent plus confortables.
Mais le domaine évolue vite — Zephyr 3.x, PREEMPT_RT en mainline, Matter sur microcontrôleurs, eBPF qui commence à pointer dans l'embarqué. C'est précisément ce qui le rend intéressant.
Un projet avec une contrainte firmware ?
De la config PSM/eDRX LTE-M à un driver custom Linux, on intervient sur la partie logicielle de vos systèmes embarqués. Zephyr, kernel Linux, BSP — on connaît le terrain.