Merlin : un framework de drivers userspace embarqués sécurisé
Dans un projet embarqué, écrire un driver n’est rarement qu’un problème de registres. Il faut aussi raccorder correctement le matériel décrit dans le DeviceTree, gérer le cycle de vie du périphérique, éviter les séquences d’initialisation fragiles, partager les bus proprement entre couches logicielles, et conserver une API stable quand le SoC ou l’IP changent.
C’est précisément le rôle de Merlin, le framework de drivers de Camelot-OS. Son idée directrice est simple : laisser au driver le code spécifique au matériel, et confier au framework tout ce qui relève de l’intégration plateforme. En pratique, cela donne une base plus légère à maintenir, plus prévisible à l’exécution, et nettement plus sûre à faire évoluer.
Le bon niveau d’abstraction pour des drivers embarqués
Merlin ne cherche pas à masquer le matériel derrière une couche opaque. Au contraire, le framework conserve les parties critiques (programmation des registres, gestion des timeouts, séquencement des contrôleurs, états de protocole) dans le driver lui-même.
Ce qu’il factorise, c’est tout le reste :
- la résolution des métadonnées issues du DeviceTree ;
- l’enregistrement des drivers à partir d’un label runtime ;
- le mapping mémoire et la configuration GPIO/pinctrl ;
- le routage des IRQ vers la bonne instance de driver ;
- une surface d’API unifiée par famille de bus.
Le résultat est particulièrement intéressant pour les équipes embarquées qui veulent éviter l’effet « BSP monolithique » : le code haut niveau reste portable, tandis que le code bas niveau garde la maîtrise fine du silicium. Le driver devient portable entre les SoCs, la résolution des périphériques dont la tâche est propriétaire se fait au travers d’un simple label, porté dans le dts du projet.
Une intégration plus rapide, sans sacrifier le contrôle
Le modèle d’usage de Merlin est volontairement strict et répétable : probe, init, opérations runtime, puis release. Ce cycle est identique pour I2C, SPI, USART, CAN ou encore USB.
drv_status_t my_probe(uint32_t label);
drv_status_t my_init(...);
drv_status_t my_release(uint32_t label);
Ce pattern unique apporte un gain immédiat en productivité. Quand on ajoute un nouveau driver, on ne réinvente pas la plomberie d’initialisation, de mapping, de pinmux ou de désenregistrement. Le développeur implémente surtout la logique du contrôleur : choix des timings, enchaînement des transactions, gestion des flags matériels, vidage de FIFO, etc.
Autrement dit, Merlin réduit le coût d’intégration sans transformer le driver en boîte noire.
Là où Merlin gagne vraiment en efficacité
Pour un développeur embarqué, l’efficacité n’est pas qu’une question de vitesse CPU. C’est aussi le temps gagné sur la maintenance, le bring-up et le débogage. Sur ce point, Merlin cumule plusieurs avantages concrets.
1. Une API stable par famille de bus
L’application s’appuie sur des symboles unifiés comme i2c_probe(), spi_xfer(), usart_write() ou can_send(). Les implémentations concrètes peuvent rester spécifiques au fondeur, puis être exportées via alias.
Ce détail compte : il découple le code applicatif des noms internes d’un driver STM32, d’un futur backend RISC-V, ou d’une autre variante SoC. On change l’implémentation, pas le contrat logiciel.
2. Un DeviceTree exploité comme source de vérité
Merlin consomme le DTS au build, génère les métadonnées nécessaires, puis remplit les structures runtime lors du register(). Le driver ne reçoit pas d’adresse de base codée en dur ni de table d’IRQ passée à la main : il reçoit un label, et le framework résout le reste.
Cette approche évite une grande partie des erreurs classiques de duplication entre code C, configuration board et intégration applicative.
3. Une construction modulaire avec Meson et Kconfig
Le projet assemble libmerlin.a via Meson, avec sélection conditionnelle des sources selon les symboles Kconfig. Les headers privés issus du DeviceTree sont générés automatiquement. Pour une base multi-cartes ou multi-configurations, c’est un vrai levier : le binaire n’embarque que ce qui correspond à la configuration active.
4. Des helpers d’E/S et de polling homogènes
Les accès registres passent par merlin_ioread*() et merlin_iowrite*(). Les transitions matérielles sont protégées par des polls bornés comme merlin_iopoll32_until_set() ou merlin_iopoll32_until_clear().
Ce point est essentiel : on garde un code proche du matériel, mais avec une discipline commune sur les accès MMIO et les attentes actives. Cela améliore la lisibilité et, surtout, la prévisibilité du comportement en phase de bring-up et d’erreur.
La sécurité, au sens embarqué du terme
Quand on parle de sécurité dans un framework de drivers, il ne s’agit pas seulement de cybersécurité au sens réseau. Il s’agit aussi d’isolement, de réduction de la surface d’erreur, et de comportements déterministes face aux mauvais états.
Merlin apporte plusieurs mécanismes intéressants à ce niveau.
L’isolation par ownership
Le lien entre une tâche et ses périphériques n’est pas implicite. Il est porté par le DeviceTree via sentry,owner, en cohérence avec CONFIG_TASK_LABEL. Au build, Merlin filtre les périphériques pour ne générer que les métadonnées des devices appartenant à la tâche considérée.
Pour un OS comme Camelot-OS, c’est une propriété forte : le driver userspace n’a pas à connaître ni parcourir l’ensemble de la plateforme. Il n’opère que sur ce qui lui est explicitement attribué.
Un modèle fail-fast
Les opérations runtime doivent échouer explicitement si probe ou init n’ont pas été exécutés correctement. Les erreurs sont normalisées via drv_status_t : état invalide, mauvais paramètre, timeout, configuration non supportée, ressource non enregistrée, etc.
Ce choix paraît simple, mais il évite deux classes de bugs fréquentes en embarqué : les séquences d’appel tolérées par accident, et les erreurs silencieuses qui n’apparaissent qu’en intégration système.
Un routage IRQ centralisé
Merlin centralise la correspondance entre numéro d’IRQ et instance de driver via merlin_platform_driver_irq_displatch(). L’application peut donc garder une boucle d’événements simple, sans répliquer partout la logique de routage.
Moins de duplication sur ce chemin critique signifie moins de code fragile, moins de tables parallèles à maintenir, et moins de divergences entre drivers.
Pas d’adresses brutes passées à la volée
Le fait que l’API prenne des labels et des structures typées, plutôt que des adresses physiques ou des descripteurs bricolés à l’exécution, réduit nettement le risque d’erreurs de câblage logiciel. Le binding entre code et matériel reste explicite, mais il est sécurisé par les métadonnées générées à partir du DTS.
Un exemple très concret : pourquoi ce modèle aide au quotidien
Prenons un driver I2C. Avec Merlin, l’instance est enregistrée via un label, les informations de base (reg, IRQ, pinctrl, horloge) viennent du DeviceTree, puis le driver se concentre sur son vrai sujet : mode d’adressage, gestion de TXIS/RXNE/TC/STOPF, traitement de NACK/BERR/ARLO/OVR, timeouts et reprise sur conditions transitoires.
Même logique côté SPI, USART ou CAN :
- le framework gère l’attache à la plateforme ;
- le driver gère la logique du contrôleur ;
- l’application consomme une API cohérente.
Pour une équipe qui maintient plusieurs drivers ou plusieurs variantes de cartes, ce découpage change vraiment l’économie du projet.
Un point intéressant : l’ouverture vers Rust
Merlin commence à intégrer le support Rust natif. Il pérvoit de fournir une crate dédiée. Merlin peut aussi piloter la construction du crate Rust camelot-merlin via Meson pour une cible embarquée thumbv8m.main-none-eabi, en injectant les entrées DTS dans le build Rust. On ne parle pas d’une application C avec une bibliothèque Rust, mais d’une application Rust complète, grace à la crate uapi de sentry et shield, qui implémente des crates entièrement native, permettant d’implémenter des applications entièrement Rust, sans jamais, à aucun moment, l’utilisation d’extern C. Ce modèle a déjà été intégré et validé dans l’écosystème Camelot-OS, où des tâches Rust et des tâches C sont aptes à cohabiter sur un même environnement.
Ce n’est pas un détail anecdotique. Cela montre que l’architecture de Merlin n’est pas enfermée dans une chaîne legacy : elle peut servir de base à une évolution progressive vers des composants Rust, tout en conservant le même socle de description matérielle et d’intégration build.
Pourquoi Merlin mérite l’attention des développeurs embarqués
Merlin ne vend pas une abstraction magique. Il propose quelque chose de plus utile dans un contexte industriel : un contrat strict entre l’application, les drivers userspace et les services plateforme de l’OS.
Ce contrat apporte trois bénéfices concrets :
- Plus d’efficacité : moins de boilerplate, moins de duplication, une intégration reproductible par famille de bus.
- Plus de robustesse : états explicites, erreurs typées, polling borné, initialisation disciplinée.
- Plus de sûreté d’intégration : ownership des devices, binding DTS strict, routage IRQ centralisé, API découplée des implémentations fondeur.
Pour les développeurs embarqués qui veulent conserver la main sur le matériel sans replonger dans un BSP dispersé et fragile, Merlin propose un compromis particulièrement solide.
La documentation de Merlin est déjà disponible ici : https://camelot-merlin.readthedocs.io/en/latest/index.html Le dépôt source de Merlin est présent ici :https://github.com/camelot-os/merlin/
