Pour ne pas réinventer la roue : une introduction aux intergiciels embarqués

[APPLICATION GREEN SHOE GARAGE - MOUSER] L’intergiciel ou le middleware est devenu le multiplicateur de force discret du développement embarqué moderne. S’il est si puissant, ce n’est pas parce qu’il élimine la complexité, mais bien parce qu’il la structure et la canalise. Avec ll’apport d’une couche d’abstraction fiable, cet intergiciel permet aux développeurs de se concentrer sur la logique applicative, plutôt que de se débattre avec l’affectation des broches, les arbres d’horloge et les singularités du silicium bas niveau. Utilisé avec discernement, il apporte la portabilité, accélère le développement et garantit la maintenabilité à long terme sur des plateformes matérielles en constante évolution.

Auteur : Michael Parks Ingénieur conseil en technologie

Fondateur de Green Shoe Garage

(Article publié sur le blog de Mouser)

 

 

L’une des règles d’or de l’ingénierie est de ne jamais réinventer la roue. Pour des raisons évidentes (réduction des coûts, délais serrés, fiabilité…), la réutilisation logicielle est une pratique d’excellence qui a fait ses preuves en génie logiciel. Dans la conception de systèmes embarqués, l’intergiciel (ou middleware) est la couche logicielle qui s’intercale entre le matériel et le code de l’application ou de la fonctionnalité finale.

Si l’on devait faire analogie avec le monde de la menuiserie, un intergiciel est une sorte de gabarit : il ne s’agit ni de la matière première - le matériel ou le système d’exploitation temps réel (RTOS) - ni de l’objet fini i.e l’application mais plutôt d’un outil qui permet un travail fiable et reproductible.

L’intergiciel fournit une couche d’abstraction permettant à un même code applicatif de s’exécuter sur diverses combinaisons de plateformes matérielles et de RTOS. Il intègre des sous-systèmes préconstruits (appelés piles ou stacks) pour de nombreuses fonctionnalités, parmi lesquelles : la pile réseau TCP/IP, un périphérique/hôte USB, un lien Bluetooth/Bluetooth Low Energy, le système de fichiers (FAT, LittleFS), un frameworks pour les graphiques et l'interface utilisateur, des pipelines audio, une couche de sécurité (TLS, moteurs cryptographiques), etc.

 

 

L’intergiciel permet aux développeurs de se concentrer sur la couche applicative, là où se concentre la valeur métier, tout en facilitant le portage de l’application sur différents RTOS ou plateformes matérielles sous-jacentes (Source : Green Shoe Garage)

 

 

L’intergiciel revêt en fait un rôle de traducteur, comme l’illustre l’exemple qui suit. Supposons que nous concevions un dispositif IoT doté d’une connectivité sans fil. Idéalement, le code applicatif devrait s’exécuter de la même manière sur un microcontrôleur (MCU) 32 bits ou sur un système sur puce (SoC) sans fil, quel qu’en soit la gamme ou le fabricant.

Or, dans la pratique, le matériel impose des caractéristiques propres et hautement spécifiques : affectation des broches, commandes au niveau des registres, cartographie des périphériques, arbres d’horloge, comportement DMA, structure d’interruptions, piles RF et modes de consommation.

Si ces spécificités "fuient" directement dans la couche applicative, la base de code se transforme rapidement en une soupe de #ifdef, de contournements spécifiques à la carte et d’hypothèses fragiles. C’est là que l’intergiciel prouve toute son utilité.

Il fournit notamment une commande standard au développeur (par exemple send_message[ ]), puis gère la complexité matérielle requise pour transmettre les données. Derrière ce simple appel, l’intergiciel orchestre l’ensemble du processus : Il sélectionne le pilote approprié, gère les tampons, traite les essais et les temporisations (timeouts), se coordonne avec l’ordonnanceur de l'OS temps réle et appelle les bonnes routines propres au matériel à travers la couche d’abstraction matérielle (HAL).

L’application ne voit pas le processus d’écriture des registres, les gestionnaires d’interruptions ou les transitions d’état radio : elle bénéficie seulement d’un service de communication fiable. C’est là que réside un avantage crucial : en cas de modification du matériel, la majeure partie du code applicatif demeure inchangée.

Dans les architectures de type Zephyr, les spécificités matérielles sont par exemple répertoriées dans l’arbre de périphériques (devicetree), qui décrit la carte et le circuit (SoC), tandis que les fonctionnalités logicielles sont sélectionnées via Kconfig.

Les modifications liées au matériel restent ainsi confinées au BSP (board support package) ou à la couche HAL, ce qui préserve presque intégralement la logique applicative. Comme nous le mentionnions plus haut, l’intergiciel fait ici office de couche de traduction : il convertit les intentions de l’application (p. ex."envoie ces données") en actions spécifiques pour la plateforme matérielle (p. ex. "commute ces registres", "gère ce transfert DMA", "attends cette interruption", "réessaie en cas d’échec").

Il découple ainsi les opérations du système du fonctionnement matériel. C’est précisément cette séparation qui garantit la portabilité, l’évolutivité et la maintenabilité à long terme des systèmes embarqués modernes.

Le dilemme de l’ingénierie : concevoir ou intégrer

Bien que l’intergiciel doive être envisagé pour chaque système embarqué, il existe des cas où sa mise en œuvre s’avérerait disproportionnée par rapport aux besoins réels. En voici quelques exemples. 

Avec des micrcontrôleurs (MCU) ultra-compacts - comme les composants 8 bits ou les MCU disposant de moins de 32 Ko de mémoire Flash- dont l’empreinte mémoire d’une pile générique s’accaparerait une part disproportionnée des ressources disponibles.

Avec des boucles de régulation en temps réel dur exigeant des garanties temporelles déterministes, au cycle près, où la moindre couche d’abstraction introduit une gigue (jitter) ou une latence inacceptable.

Avec des dispositifs hautement spécialisés ou uniques (one-off), dont les fonctionnalités sont strictement définies et pour lesquels la portabilité et la réutilisation ne constituent pas des objectifs de conception pertinents.

Dans ces configurations, les développeurs tendent plutôt à concevoir des couches légères et sur mesure. Déterminer s’il faut adopter un intergiciel tiers ou développer une solution personnalisée constitue ici un arbitrage architectural critique.

Bien que l’intergiciel accélère le développement, il impose des dépendances et une courbe d’apprentissage. Les équipes techniques qui envisagent un intergiciel sur étagère doivent avant toute chose évaluer les critères suivants :

- Complexité des protocoles : il faut éviter d’avoir à réimplémenter des piles protocolaires standardisées comme le Bluetooth Low Energy ou l’USB. Les programmes de qualification et de conformité (en l’occurrence la qualification du Bluetooth SIG et la conformité de l’USB-IF) comportent des exigences de test et d’interopérabilité qui s’avèrent coûteuses lorsqu’il s’agit d’une pile sur mesure. À l’inverse, l’utilisation de piles qualifiées ou de kits de développement logiciel (SDK) fournis par les fabricants réduit généralement les délais et les risques.

- Certification réglementaire : l’adoption de piles précertifiées permet de réduire considérablement les moyens nécessaires à la certification ainsi que les risques associés. Par exemple, les produits peuvent hériter d’identifiants de conception qualifiée Bluetooth – ou QDID, pour Qualified Design ID – en utilisant des composants qualifiés non modifiés issus des SDK des fabricants. Par ailleurs, les développements liés à la sûreté de fonctionnement tirent parti du respect du cycle de vie défini par la norme IEC 61508.

- Contraintes de ressources (Flash/RAM) : les intergiciels génériques sont rarement optimisés pour offrir la plus faible empreinte mémoire possible. Si chaque octet de mémoire Flash compte, une implémentation sur mesure et épurée peut s’avérer nécessaire.

- Visibilité lors du débogage : l’intergiciel se comporte parfois comme une "boîte noire".  Lorsqu’un bug survient au cœur d’une pile USB tierce, les équipes doivent disposer de l’expertise et des outils requis pour dérouler le code externe au pas à pas.

- Expertise de l’équipe et coûts du cycle de vie : l’intergiciel réduit l’effort de développement initial, mais déplace les coûts vers les phases de configuration, d’intégration et de mise à jour continue. Si une solution sur mesure s’avère parfois plus rapide à développer au départ, elle se transforme à long terme en une obligation de maintenance que seule votre équipe est à même de prendre en charge. Le choix final dépend de l’expérience des équipes, de la durée de vie prévue du produit et de la fréquence de mise à niveau de la plateforme.

 

 

Le choix entre concevoir, intégrer ou acheter dépend de multiples critères qui dépassent le cadre de la simple question technique (Source : Green Shoe Garage)

 

Le défi des abstractions perméables

Les abstractions perméables font partie de ces défis incontournables du génie logiciel. De fait, toute abstraction relativement complexe finit par laisser transparaître les détails sous-jacents, et les API d’intergiciel n’y font pas exception.

Bien que l’intergiciel s’efforce de présenter une interface épurée et indépendante du matériel, les systèmes embarqués imposent des contraintes physiques et architecturales que le logiciel ne peut pas entièrement dissimuler. Les comportements temporels, l’organisation de la mémoire, les limites des périphériques et les singularités du silicium influencent inévitablement les couches supérieures de la pile.

Par conséquent, même les abstractions d’intergiciel les mieux conçues sont rarement étanches dans la pratique. Les contraintes physiques du matériel (latence, exigences d’alignement, transitions d’état énergétique, schémas d’accès mémoire…) "filtrent" souvent à travers la frontière d’abstraction, en particulier dans des conditions limites ou lors de pics de performance.

La fonction générique file_write(), fournie par un intergiciel de système de fichiers, en est un exemple typique. Sur de nombreuses plateformes, cette abstraction se comporte de façon prévisible.

Cependant, lorsque le support de stockage sous-jacent est de la mémoire Flash NAND brute, l’application peut soudainement devoir prendre en compte la taille des blocs d’effacement, les limites de pages ou l’impact sur les performances du nivellement d’usure, qui constituent autant de caractéristiques fondamentales du matériel.

Aucune couche logicielle ne peut totalement occulter le fait que la mémoire Flash NAND doit être effacée par blocs entiers, ni que l’amplification d’écriture dégrade la latence et l’endurance. Dans de telles configurations, l’abstraction d’une "simple écriture de fichier" atteint ses limites, ce qui rend indispensable la compréhension du matériel.

Ce phénomène se vérifie dans de nombreux sous-systèmes. Les abstractions réseau peuvent laisser filtrer de la latence ou des effets de mise en mémoire tampon. De même, les périphériques pilotés par accès direct à la mémoire (DMA) peuvent imposer des contraintes d’alignement.

Enfin, la cohérence des caches peut impacter la visibilité des données entre les différents contextes d’exécution. Au vu de ces multiples complications, force est de constater que si l’intergiciel réduit la complexité, il n’exclut en rien la nécessité de comprendre le système sous-jacent. Dès lors, exploiter efficacement un intergiciel exige bien plus qu’une connaissance superficielle de l’API.

Les ingénieurs doivent également appréhender les hypothèses que l’intergiciel formule sur le matériel et l’environnement d’exécution. Une couche d’intergiciel bien conçue limite ces hypothèses au strict minimum et délègue les comportements spécifiques au matériel aux couches inférieures.

Au sein d’une architecture embarquée correctement stratifiée, l’intergiciel ne doit ni intégrer de données codées en dur, ni dépendre de détails matériels de bas niveau. Le non-respect de ce principe entraîne une rupture de l’architecture en couches.

Éviter les fuites

Les bibliothèques d’intergiciels doivent rester portables et indépendantes du matériel. Pour ce faire, elles gèrent les spécificités des composants en s’appuyant sur le BSP et la HAL. Les catégories d’informations suivantes ne doivent en aucun cas filtrer dans le code de l’intergiciel.

- Connexions spécifiques à la carte (périmètre du BSP) : les numéros de broches physiques, les affectations des GPIO et les détails de routage du PCB relèvent du BSP. Par exemple, le choix de la broche du MCU qui pilote une LED ou une ligne de sélection de puce (Chip Select) est une décision propre à la carte. Les couches supérieures doivent appeler des fonctions du BSP plutôt que de manipuler des numéros de broches particuliers. Un intergiciel dépendant du mappage des broches opère au mauvais niveau d’abstraction.

- Comportements spécifiques au microcontrôleur (périmètre de la HAL) : la configuration de l’arbre d’horloge, les registres de temporisateurs, les vecteurs d’interruption, les contournements d’errata et les séquences de gestion de l’énergie relèvent de la HAL. La HAL existe pour traduire des opérations génériques en actions propres au silicium. L’intergiciel doit appeler les API de la HAL – par exemple flash_write() ou uart_tx() – et non interagir directement avec des registres ou des constantes matérielles.

- Détails des instances de périphériques et des domaines de tension : le choix d’utiliser UART1 ou UART2, ou le domaine de tension dont dépend un périphérique, doit être résolu en dessous de la couche d’intergiciel. Ces détails sont configurés dans la HAL, le BSP ou les fichiers de configuration à la compilation. L’intergiciel doit opérer sur des identifiants ou des descripteurs abstraits, et non sur des instances de périphériques particulières.

- Contraintes temporelles et d’alignement : les exigences relatives à l’alignement des tampons, au comportement du cache ou à la latence découlent souvent du matériel. Plutôt que d’intégrer ces hypothèses dans la logique applicative, elles doivent être gérées dans les couches inférieures ou exprimées via des paramètres de configuration. Un système correctement stratifié fait en sorte que l’application n’ait pas à gérer les limites de lignes de cache ou les règles d’alignement DMA.

Atténuer l’impact des fuites

Si un composant d’intergiciel théoriquement générique dépend directement de numéros de broches, d’adresses de registres, de temporisations liées à l’horloge ou d’hypothèses de tension, il viole le principe de séparation des préoccupations. Un intergiciel qui  "a connaissance" des broches, des horloges ou des rails d’alimentation perd toute portabilité. Ce couplage fort nuit à la réutilisabilité et complique considérablement les changements de plateforme.

Lorsque le matériel évolue, ces hypothèses masquées ont tendance à faire surface, ce qui entraîne des modifications imprévues dans un code applicatif censé être indépendant du matériel. Les abstractions perméables ne pouvant être totalement éliminées, les ingénieurs embarqués chevronnés s’attachent à les confiner et à les gérer.

L’objectif ici est de circonscrire les connaissances propres au matériel plutôt que de les laisser se propager de manière imprévisible au sein de la base de code.

Plusieurs bonnes pratiques permettent d’atténuer l’impact de ces fuites d’abstraction.

- Isoler les paramètres matériels dans des fichiers de configuration : les valeurs dépendantes du matériel doivent être centralisées dans des en-têtes de configuration ou des modules dédiés, plutôt qu’intégrées dans la logique applicative. Ainsi, si un intergiciel de système de fichiers doit connaître la taille de bloc d’effacement ou de page de la mémoire Flash, ces valeurs doivent être définies dans un fichier de configuration (p. ex. : #define NAND_BLOCK_SIZE 4096). Cela rend les hypothèses matérielles explicites, documentées et faciles à modifier au moment de changer de matériel.

- Maintenir des frontières de couches nettes (usage du BSP et de la HAL) : le logiciel doit être structuré de façon à ce que seuls le BSP et la HAL interagissent directement avec le matériel. L’intergiciel doit faire appel à ces couches plutôt qu’accéder lui-même aux ressources matérielles. Si une pile réseau doit réinitialiser un module radio, elle doit appeler une fonction du BSP. Si une couche de stockage doit effacer de la mémoire Flash, elle doit appeler un service de la HAL. Cette discipline empêche les détails matériels de s’infiltrer vers les couches supérieures et simplifie le portage vers de nouvelles plateformes.

- Documenter les hypothèses et les contraintes : l’intergiciel repose inévitablement sur des hypothèses concernant le timing, la disponibilité mémoire ou le contexte d’exécution. Ces contraintes doivent être explicitement documentées. Une documentation claire aide les développeurs à comprendre à quel moment les abstractions atteignent leurs limites et leur permet de concevoir les systèmes en tenant compte de ces restrictions, plutôt que de les laisser découvrir ces limites qu’une fois que des défaillances surviennent tard dans le cycle de développement.

- Intégrer la compréhension des couches inférieures : l’intergiciel réduit la quantité de code spécifique au matériel que les ingénieurs doivent écrire, mais il ne supprime pas la nécessité de comprendre le matériel. Lorsque des problèmes surviennent, le débogage exige souvent de tracer le comportement à travers l’intergiciel et la HAL, jusqu’au silicium. Dès lors, les équipes qui traitent l’intergiciel comme un substitut complet à la connaissance du matériel se trouvent en difficulté dès que des cas limites apparaissent.

En résumé, les abstractions perméables sont une caractéristique inhérente aux systèmes embarqués et non un défaut de conception. À condition d’imposer des frontières de couches nettes, d’isoler les hypothèses matérielles et de savoir parfaitement ce que les abstractions garantissent (ou non), les ingénieurs sont en mesure de contenir efficacement ces fuites.

L’intergiciel reste un outil puissant pour la productivité et la réutilisation, à condition d’être utilisé avec rigueur architecturale et une parfaite compréhension des réalités matérielles qui le sous-tendent.

L’intergiciel et l’avenir

Le développement embarqué délaisse l’intégration manuelle au profit d’écosystèmes centrés sur la configuration. Les plateformes modernes comme Zephyr OS, FreeRTOS, tout comme les plateformes propriétaires telle que le SDK nRF Connect de Nordic, intègrent désormais le RTOS, l’intergiciel et les systèmes de build dans un environnement cohérent et unifié.

Alors qu’autrefois, les développeurs pouvaient passer des semaines à imbriquer des piles TCP/IP et des systèmes de fichiers disparates, tout cela se gère aujourd’hui via une configuration de haut niveau. Le rôle du développeur a évolué, passant de l’écriture de "glue code" à la gestion d’une configuration structurée.

Malgré une courbe d’apprentissage parfois raide, le retour sur investissement est considérable : moins de code passe-partout (boilerplate), moins de bizarreries propres aux fabricants, et surtout des écosystèmes qui privilégient la logique applicative centrale à la plomberie.

Cette transition est d’autant plus marquante pour l’industrie qu’elle accélère le délai de mise sur le marché. En proposant désormais des piles logicielles pré-validées incluant pilotes, protocoles réseau et fonctionnalités de sécurité prêts à l’emploi, les fabricants mettent des outils de niveau professionnel à la portée des startups les plus modestes.

De plus, à mesure que le développement converge autour des grandes plateformes open source, les correctifs de sécurité et les bonnes pratiques se propagent plus rapidement, ce qui a pour effet de relever le niveau de référence en matière de qualité et de conformité.

Bien que cette consolidation crée une certaine dépendance vis-à-vis des feuilles de route de ces plateformes, les avantages du soutien communautaire et de la réduction des risques d’intégration l’emportent largement sur les inconvénients d’une dépendance exclusive à un fournisseur.

Pour s’adapter à cette réalité, les ingénieurs ont donc tout intérêt à adopter ces plateformes intégrées plutôt que de réinventer la roue à chaque nouveau projet. Dans cet environnement moderne, il ne suffit plus de bien connaître le langage C : la maîtrise des outils de gestion de configuration est devenue indispensable pour exploiter le plein potentiel des piles préintégrées. Les développeurs doivent également ériger la maintenance en priorité.

Ce n’est qu’en suivant les évolutions de l’écosystème qu’ils pourront bénéficier des derniers correctifs de sécurité et des nouvelles fonctionnalités et ainsi s’épargner toute dette technique. Enfin, le fait de choisir une plateforme forte d’une communauté active et d’y participer, tant pour bénéficier de support que pour en offrir vous-même, s’avère indispensable pour garantir la pérennité et la viabilité de vos futurs projets.