Rust est-il prêt pour les microcontrôleurs ?
Le C et le C++ ont été le langage des microcontrôleurs pendant des décennies, à l'exception de quelques fonctions assembleur optimisées à la main. Quelles sont donc les opportunités de Rust, quels sont ses avantages et devriez-vous commencer à apprendre ce nouveau langage ?
Le C et le C++ ont été le langage des microcontrôleurs pendant des décennies, à l'exception de quelques fonctions assembleur optimisées à la main. Quelles sont donc les opportunités de Rust, quels sont ses avantages et devriez-vous commencer à apprendre ce nouveau langage ?
Depuis des décennies, le langage C est le langage préféré pour les systèmes embarqués. Grâce à sa capacité à accéder à la mémoire et à la manipuler directement, il a été adopté comme alternative à l'assembleur. Le C++ a également eu un impact sur les microcontrôleurs plus petits grâce à Arduino, encourageant une approche orientée objet pour la conception de bibliothèques. Les développeurs ont bénéficié d'une multitude de bibliothèques faciles à intégrer et à déployer, prenant en charge les applications USB, les protocoles sans fil et l'interaction avec des capteurs externes.
Mais ces langages ont leurs limites. Ils sont excellents en bas niveau mais, par conséquent, ils ne supportent pas les aspects de haut niveau tels que le décodage de JSON ou de XML. Les environnements tels qu'Arduino facilitent le partage des bibliothèques ; sinon, il n'existe pas de dépôts centraux pour les bibliothèques C/C++ avec des interfaces de programmation d'application (API) formalisées. Et, en tant que programmeur pour de l'embarqué, vous ne penseriez jamais à utiliser les bibliothèques d'allocation de mémoire disponibles ou des fonctionnalités telles que printf.
Il y a ensuite toutes les erreurs fantastiques que vous pouvez faire avec les pointeurs et les variables non initialisées. Grâce à cette capacité linguistique, les programmeurs peuvent accéder à n'importe quelle variable, fonction ou registre, à moins que la puce du microcontrôleur ne l'empêche en raison de mesures de sécurité. Bien que cela soit parfois une aubaine, une ligne de code mal formée peut être à l'origine d'un grand nombre de problèmes.
Par exemple, lors de l'initialisation d'une variable de pointeur en C, il convient de lui attribuer NULL (techniquement connue sous le nom de constante de pointeur nulle), si la valeur réelle n'est pas actuellement disponible. Les fonctions utilisant des pointeurs doivent vérifier la présence de NULL avant de tenter d'utiliser une variable ou un pointeur de fonction. Cependant, soit cette vérification n'est pas effectuée, soit les programmeurs oublient de l'ajouter. Il existe également des microcontrôleurs pour lesquels 0 (zéro), la valeur de NULL, est un emplacement de mémoire ou de code valide.
Dans Rust, NULL n'existe pas. Au lieu de cela, il existe un enum (type énuméré) qui contient soit une valeur, soit aucune valeur. Non seulement il peut être utilisé avec les pointeurs, mais il peut également être utilisé dans de nombreux autres cas, tels que la valeur de retour d'une fonction qui n'a rien (plutôt que 0) à renvoyer. Ceci est démontré dans la fonction de division suivante, qui gère proprement les situations potentielles de division par zéro.
Dans cette fonction de division, si le dénominateur est 0,0, la valeur de retour None indique qu'aucune réponse ne peut être fournie. Le code appelant la fonction peut vérifier si la valeur de retour est None ou Some (T), c'est-à-dire une valeur de réponse valide.
La sécurité des types est également très stricte, garantissant que les catégories de données des variables correspondent à ceux des autres variables ou des littéraux que le programmeur peut essayer de leur assigner. Ainsi, toute tentative d'ajout implicite d'un entier à une chaîne de caractères sera signalée comme une erreur lors de la compilation. Un autre objectif du langage est la concurrence. Il s'agit de la capacité du compilateur à modifier l'ordre d'exécution du code. Par exemple, un code peut comprendre une addition, une soustraction et le cosinus de l'angle (dans cet ordre). Rust peut réorganiser ce calcul si le résultat correct peut toujours être obtenu. Cependant, cette capacité est destinée aux systèmes multi-cœurs et multi-processeurs et est moins applicable aux petits systèmes embarqués.
En théorie, oui, mais il y a quelques difficultés pratiques. Le premier est la chaîne d'outils. La plupart des développeurs ont utilisé gcc pour compiler leur code en C. Cependant, Rust utilise LLVM. Plutôt que d'être un compilateur, LLVM est une structure pour la création de compilateurs. Par exemple, en utilisant Clang et LLVM, il est possible de compiler du code C pour un microcontrôleur. De nombreux fabricants ont adopté une chaîne d'outils basée sur LLVM, en particulier ceux qui proposent des processeurs Arm Cortex. S'il n'y a pas de support LLVM pour votre appareil préféré, vous n'utiliserez pas Rust de sitôt.
Le prochain défi consiste à obtenir le code qui permet d'accéder aux registres périphériques dans Rust. Cela nous amène à une discussion sur les crates.
Alors que le code est écrit en Rust, ce sont les crates qui emballent le tout. Une crate contient le code source et les fichiers de configuration de votre projet. Et si vous voulez le support d'une carte de développement, comme le micro:bit, vous aurez besoin de sa crate. Il existe également une crate pour les périphériques du microcontrôleur Nordic de la série nRF51 sur cette carte. Enfin, il y a une crate spécifique pour le processeur Arm Cortex-M qui l'alimente.
Un autre aspect intéressant des crates est qu'il y a beaucoup de code déjà disponible pour des tâches génériques, comme l'implémentation d'I2C ou l'interfaçage avec des capteurs SPI. En utilisant l'approche des crates, vous pouvez lister toutes les crates utilisés dans votre projet, et même noter leur numéro de version, afin que les autres sachent quelle version fonctionnait lors de la création du projet. Les crates sont généralement stockés dans un dépôt central en ligne (crates.io), de sorte qu'elles sont faciles à acquérir.
Les littéraux, c'est-à-dire les valeurs que nous attribuons aux variables et aux constantes, sont souvent difficiles à déchiffrer, en particulier lorsque votre vision commence à se détériorer, soit à cause de l'âge, soit à cause du nombre d'heures que vous avez passées à l'écran dans la journée. Les nombres binaires, hexadécimaux, et même les grands nombres et constantes peuvent involontairement gagner ou perdre un chiffre ou une décimale. Rust s'attaque à ce problème en autorisant l'utilisation de tirets bas pour diviser la valeur en morceaux gérables. Le trait de soulignement est utilisé uniquement pour améliorer la lisibilité et ne joue aucun autre rôle. Cela peut également être défini à l'aide d'un suffixe.
Il est intéressant de noter qu'il peut y avoir des dépassements d'entiers en Rust, mais que ce comportement est controlé lors de la compilation. Les dépassements provoquent une "panique" lors de la compilation en mode débogage, mais sont autorisés avec le programme finalisé. Cependant, certaines méthodes peuvent supporter des dépassements explicites, un wrapper ou la saturation. Il est également possible de renvoyer None en cas de dépassement en utilisant la méthode checked.
En plus du langage, il existe également une multitude d'outils utiles. Cargo est à la fois le gestionnaire de construction et de paquets. Rustfmt formate votre code source avec la bonne indentation. Ensuite il y a Clippy, un outil d'analyse statique du code qui recherche les constructions de code étranges et les bogues éventuels.
Enfin, les développeurs voudront sans doute intégrer du code C/C++ existant. C'est possible grâce à l'interface de fonction étrangère (FFI).
Il y a plusieurs options qui s'offrent à vous si vous souhaitez comprendre les capacités de Rust dans le domaine des systèmes embarqués. Rust fonctionne sur Raspberry Pi et peut contrôler les interfaces disponibles, ce qui permet de faire clignoter une LED assez facilement. Vous pouvez également créer du code pour le micro:bit et la famille STM32 est bien prise en charge. Si vous n'avez pas de matériel sous la main, vous pouvez essayer de faire tourner un microcontrôleur émulé à l'aide de QEMU.
Si vous êtes curieux de savoir s'il existe des systèmes d'exploitation en temps réel (RTOS), vous pouvez consulter Bern ou OxidOS (que Stuart Cording a également interviewé au salon embedded world). Il existe également un moyen de fournir au code Rust un accès à FreeRTOS. Pour une liste de projets, consultez le site https://arewertosyet.com/.
Malgré cela, il n'est jamais mauvais d'élargir son horizon, surtout si l'on a des dizaines d'années de carrière devant soi. Grâce à l'internet, aux ressources disponibles gratuitement et à l'air du temps, votre premier projet avec Rust est peut-être plus proche que vous ne le pensez.
Traduction : Laurent RAUBER
Vous souhaitez publier un article dans Elektor Mag ? Voici comment faire
Depuis des décennies, le langage C est le langage préféré pour les systèmes embarqués. Grâce à sa capacité à accéder à la mémoire et à la manipuler directement, il a été adopté comme alternative à l'assembleur. Le C++ a également eu un impact sur les microcontrôleurs plus petits grâce à Arduino, encourageant une approche orientée objet pour la conception de bibliothèques. Les développeurs ont bénéficié d'une multitude de bibliothèques faciles à intégrer et à déployer, prenant en charge les applications USB, les protocoles sans fil et l'interaction avec des capteurs externes.
Mais ces langages ont leurs limites. Ils sont excellents en bas niveau mais, par conséquent, ils ne supportent pas les aspects de haut niveau tels que le décodage de JSON ou de XML. Les environnements tels qu'Arduino facilitent le partage des bibliothèques ; sinon, il n'existe pas de dépôts centraux pour les bibliothèques C/C++ avec des interfaces de programmation d'application (API) formalisées. Et, en tant que programmeur pour de l'embarqué, vous ne penseriez jamais à utiliser les bibliothèques d'allocation de mémoire disponibles ou des fonctionnalités telles que printf.
Je m'abonne
Abonnez-vous à la balise thématique Rust pour être averti dès qu'une information relative à ce sujet sera publiée par Elektor ! Il y a ensuite toutes les erreurs fantastiques que vous pouvez faire avec les pointeurs et les variables non initialisées. Grâce à cette capacité linguistique, les programmeurs peuvent accéder à n'importe quelle variable, fonction ou registre, à moins que la puce du microcontrôleur ne l'empêche en raison de mesures de sécurité. Bien que cela soit parfois une aubaine, une ligne de code mal formée peut être à l'origine d'un grand nombre de problèmes.
Qu'est-ce-que Rust ?
Rust est un langage de programmation polyvalent relativement récent, développé par Graydon Hoare. Le projet a débuté en 2006 alors qu'il travaillait chez Mozilla et est devenu par la suite un projet indépendant. Il a été conçu pour développer des logiciels pour des systèmes tels que les DNS, les navigateurs et la virtualisation, et a également été utilisé pour un serveur Tor. Rust a plusieurs objectifs, mais le plus important est sans doute l'approche intégrée de la sécurité de la mémoire.Par exemple, lors de l'initialisation d'une variable de pointeur en C, il convient de lui attribuer NULL (techniquement connue sous le nom de constante de pointeur nulle), si la valeur réelle n'est pas actuellement disponible. Les fonctions utilisant des pointeurs doivent vérifier la présence de NULL avant de tenter d'utiliser une variable ou un pointeur de fonction. Cependant, soit cette vérification n'est pas effectuée, soit les programmeurs oublient de l'ajouter. Il existe également des microcontrôleurs pour lesquels 0 (zéro), la valeur de NULL, est un emplacement de mémoire ou de code valide.
#include <stdio.h>
int main() {
int *p= NULL; //initialize the pointer as null.
printf("The value of pointer is %u",p);
return 0;
}
int main() {
int *p= NULL; //initialize the pointer as null.
printf("The value of pointer is %u",p);
return 0;
}
Dans Rust, NULL n'existe pas. Au lieu de cela, il existe un enum (type énuméré) qui contient soit une valeur, soit aucune valeur. Non seulement il peut être utilisé avec les pointeurs, mais il peut également être utilisé dans de nombreux autres cas, tels que la valeur de retour d'une fonction qui n'a rien (plutôt que 0) à renvoyer. Ceci est démontré dans la fonction de division suivante, qui gère proprement les situations potentielles de division par zéro.
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}
// The return value of the function is an option
let result = divide(2.0, 3.0);
// Pattern match to retrieve the value
match result {
// The division was valid
Some(x) => println!("Result: {x}"),
// The division was invalid
None => println!("Cannot divide by 0"),
}
Code example from: https://doc.rust-lang.org/stable/std/option/index.html
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}
// The return value of the function is an option
let result = divide(2.0, 3.0);
// Pattern match to retrieve the value
match result {
// The division was valid
Some(x) => println!("Result: {x}"),
// The division was invalid
None => println!("Cannot divide by 0"),
}
Code example from: https://doc.rust-lang.org/stable/std/option/index.html
Dans cette fonction de division, si le dénominateur est 0,0, la valeur de retour None indique qu'aucune réponse ne peut être fournie. Le code appelant la fonction peut vérifier si la valeur de retour est None ou Some (T), c'est-à-dire une valeur de réponse valide.
La sécurité des types est également très stricte, garantissant que les catégories de données des variables correspondent à ceux des autres variables ou des littéraux que le programmeur peut essayer de leur assigner. Ainsi, toute tentative d'ajout implicite d'un entier à une chaîne de caractères sera signalée comme une erreur lors de la compilation. Un autre objectif du langage est la concurrence. Il s'agit de la capacité du compilateur à modifier l'ordre d'exécution du code. Par exemple, un code peut comprendre une addition, une soustraction et le cosinus de l'angle (dans cet ordre). Rust peut réorganiser ce calcul si le résultat correct peut toujours être obtenu. Cependant, cette capacité est destinée aux systèmes multi-cœurs et multi-processeurs et est moins applicable aux petits systèmes embarqués.
Est-ce-que je peux utiliser Rust avec mon microcontrôleur ?
En théorie, oui, mais il y a quelques difficultés pratiques. Le premier est la chaîne d'outils. La plupart des développeurs ont utilisé gcc pour compiler leur code en C. Cependant, Rust utilise LLVM. Plutôt que d'être un compilateur, LLVM est une structure pour la création de compilateurs. Par exemple, en utilisant Clang et LLVM, il est possible de compiler du code C pour un microcontrôleur. De nombreux fabricants ont adopté une chaîne d'outils basée sur LLVM, en particulier ceux qui proposent des processeurs Arm Cortex. S'il n'y a pas de support LLVM pour votre appareil préféré, vous n'utiliserez pas Rust de sitôt.
Alors que le code est écrit en Rust, ce sont les crates qui emballent le tout. Une crate contient le code source et les fichiers de configuration de votre projet. Et si vous voulez le support d'une carte de développement, comme le micro:bit, vous aurez besoin de sa crate. Il existe également une crate pour les périphériques du microcontrôleur Nordic de la série nRF51 sur cette carte. Enfin, il y a une crate spécifique pour le processeur Arm Cortex-M qui l'alimente.
Un autre aspect intéressant des crates est qu'il y a beaucoup de code déjà disponible pour des tâches génériques, comme l'implémentation d'I2C ou l'interfaçage avec des capteurs SPI. En utilisant l'approche des crates, vous pouvez lister toutes les crates utilisés dans votre projet, et même noter leur numéro de version, afin que les autres sachent quelle version fonctionnait lors de la création du projet. Les crates sont généralement stockés dans un dépôt central en ligne (crates.io), de sorte qu'elles sont faciles à acquérir.
Quels sont les autres choses intéressantes intégrées à Rust ?
De nombreux autres éléments intéressants sont intégrés à Rust, depuis le langage jusqu'aux outils.Les littéraux, c'est-à-dire les valeurs que nous attribuons aux variables et aux constantes, sont souvent difficiles à déchiffrer, en particulier lorsque votre vision commence à se détériorer, soit à cause de l'âge, soit à cause du nombre d'heures que vous avez passées à l'écran dans la journée. Les nombres binaires, hexadécimaux, et même les grands nombres et constantes peuvent involontairement gagner ou perdre un chiffre ou une décimale. Rust s'attaque à ce problème en autorisant l'utilisation de tirets bas pour diviser la valeur en morceaux gérables. Le trait de soulignement est utilisé uniquement pour améliorer la lisibilité et ne joue aucun autre rôle. Cela peut également être défini à l'aide d'un suffixe.
10000 => 10_000
0.00001 => 0.000_01
0xBEEFD00F => 0xBEEF_D00Fu32 (32-bit unsigned integer)
0b01001011 => 0b0100_1011
Further examples: https://doc.rust-lang.org/book/ch03-02-data-types.html
0.00001 => 0.000_01
0xBEEFD00F => 0xBEEF_D00Fu32 (32-bit unsigned integer)
0b01001011 => 0b0100_1011
Further examples: https://doc.rust-lang.org/book/ch03-02-data-types.html
Il est intéressant de noter qu'il peut y avoir des dépassements d'entiers en Rust, mais que ce comportement est controlé lors de la compilation. Les dépassements provoquent une "panique" lors de la compilation en mode débogage, mais sont autorisés avec le programme finalisé. Cependant, certaines méthodes peuvent supporter des dépassements explicites, un wrapper ou la saturation. Il est également possible de renvoyer None en cas de dépassement en utilisant la méthode checked.
En plus du langage, il existe également une multitude d'outils utiles. Cargo est à la fois le gestionnaire de construction et de paquets. Rustfmt formate votre code source avec la bonne indentation. Ensuite il y a Clippy, un outil d'analyse statique du code qui recherche les constructions de code étranges et les bogues éventuels.
Enfin, les développeurs voudront sans doute intégrer du code C/C++ existant. C'est possible grâce à l'interface de fonction étrangère (FFI).
use libc::size_t;
#
extern {
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}
fn main() {
let x = unsafe { snappy_max_compressed_length(100) };
println!("max compressed length of a 100 byte buffer: {}", x);
}
Example for calling C function "snappy" from Rust sourced from: https://doc.rust-lang.org/nomicon/ffi.html
#
extern {
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}
fn main() {
let x = unsafe { snappy_max_compressed_length(100) };
println!("max compressed length of a 100 byte buffer: {}", x);
}
Example for calling C function "snappy" from Rust sourced from: https://doc.rust-lang.org/nomicon/ffi.html
Comment puis-je essayer Rust ?
Comme la plupart des projets, les outils pour Rust sont open-source et disponibles gratuitement. De plus, de nombreux sites web fournissent de la documentation, des tutoriels et des conseils. L'endroit le plus simple pour commencer est sans doute votre PC. Les tutoriels vous guident tout au long du processus d'installation de la chaîne d'outils, suivi d'une introduction au langage.Il y a plusieurs options qui s'offrent à vous si vous souhaitez comprendre les capacités de Rust dans le domaine des systèmes embarqués. Rust fonctionne sur Raspberry Pi et peut contrôler les interfaces disponibles, ce qui permet de faire clignoter une LED assez facilement. Vous pouvez également créer du code pour le micro:bit et la famille STM32 est bien prise en charge. Si vous n'avez pas de matériel sous la main, vous pouvez essayer de faire tourner un microcontrôleur émulé à l'aide de QEMU.
Si vous êtes curieux de savoir s'il existe des systèmes d'exploitation en temps réel (RTOS), vous pouvez consulter Bern ou OxidOS (que Stuart Cording a également interviewé au salon embedded world). Il existe également un moyen de fournir au code Rust un accès à FreeRTOS. Pour une liste de projets, consultez le site https://arewertosyet.com/.
Est-ce-que Rust va remplacer le C pour les systèmes embarqués ?
Si vous envisagez une carrière dans le domaine des logiciels embarqués, vous craignez peut-être d'apprendre le mauvais langage. Cependant, il n'y a pas lieu de paniquer. L'industrie a eu des décennies pour acquérir des avantages similaires à ceux offerts par Rust grâce à Ada, mais ce n'est que dans l'industrie aérospatiale que ce langage a réellement percé. Traditionnellement, le monde de l'embarqué a été lent à adopter des pratiques courantes dans d'autres branches de l'industrie du logiciel, de sorte que les chances d'un changement radical vers Rust au cours de la prochaine décennie restent faibles.Malgré cela, il n'est jamais mauvais d'élargir son horizon, surtout si l'on a des dizaines d'années de carrière devant soi. Grâce à l'internet, aux ressources disponibles gratuitement et à l'air du temps, votre premier projet avec Rust est peut-être plus proche que vous ne le pensez.
Traduction : Laurent RAUBER
Vous souhaitez publier un article dans Elektor Mag ? Voici comment faire