Journal Découverte de rust pour l'embarqué

Posté par  (site web personnel, Mastodon) . Licence CC By‑SA.
2
9
mar.
2026

Rust sur microcontrôleur

J'avais déjà fait quelques essais de rust mais exclusivement pour des outils cli. Cependant mon cœur de métier et ce que j'apprécie c'est programmer des microcontrôleurs. Donc, en ce début d'année je me suis motivé, je suis sorti de ma zone de confort et j'ai écrit un petit firmware pour un stm32 en rust. Quelque chose de tout simple, un programme qui fait juste clignoter une led (l'équivalent du traditionnel hello world pour l'embarqué).

J'ai suivi différents tuto mais la plupart se contentent d'indiquer des crates à utiliser et quelles fonctions appeler. Moi je voulais comprendre comment fonctionne la cross-compilation, comprendre comment fonctionne l'édition de liens, comprendre l'utilité de chaque crate utilisé…

J'ai donc écrit un petit mémo suite à ces essais: mémo

Ce n'est pas parfait, ce n'est pas exhaustif. Je n'ai, par exemple, pas approfondi l'initialisation de la mémoire, ni l'appel de la fonction main… Mais on ne sait jamais, ça peut peut-être aider d'autres personnes.

  • # dommage

    Posté par  . Évalué à 1 (+0/-0).

    le petit mémo aurait été un parfait journal ! plutôt qu'un lien vers ton site j'imagine que si tu donne ton accord il y a moyen de corrigé cela. sinon poste dans la rubrique lien ;)

    1) poste un commentaire ici de ton mémo pour la mise en page linuxfr
    2) si un modo se sent de le recopier a la place du journal

    \o/

    • [^] # Re: dommage

      Posté par  . Évalué à 1 (+0/-0).

      Super idée, surtout que le site du mémo est bloqué par le proxy web de mon employeur … ;-)

  • # Mémo

    Posté par  (site web personnel, Mastodon) . Évalué à 2 (+1/-0).

    Sommaire

    Suite au commentaire ChocolatineFlying, voici le texte original du mémo:

    Intro

    J'ai déjà pu expérimenter l'utilisation du langage rust et de son environnement
    dans le cadre du dev d'outil cli, mais pas encore pour de l'embarqué bare metal.
    Cet article va donc me servir de mémo quant à ces essais.

    Pour ces tests, j'utiliserai une carte de dev ST nucleo stm32l031k6.
    Pour ce qui est de la toolchain rust, j'utilise la version 1.89.0.
    Vous pouvez retrouver le répertoire du projet ici.

    Premièrement petit tour de la doc:

    Système de recette

    Première bonne surprise, cargo (l'utilitaire de construction rust), gère
    nativement la cross-compilation. Pour cela, on peut soit lui passer l'option
    --target avec le code triple de la cible.
    Soit créer un fichier .cargo/config.toml à la racine
    de notre projet, dans lequel on spécifie lui indique la cible.

    Donc pour le stm32l032k6, on utilse soit:

    • cargo build --target thumbv6m-none-eabi
    • le fichier de config cargo avec: toml [build] target = thumbv6m-none-eabi Ainsi lorsqu'on appellera cargo build il utilisera la cible spécifier dans le fichier de config

    Description du matériel

    Contrairement au C, st ne fournis pas de HAL ou de LL rust. En revanche,
    la communauté rust étant très active, on trouve des crates faisant un travail
    équivalent.

    On distingue trois niveaux d'intégration

    1. les crates cortex-m et cortex-m-rt réalisant l'abstraction des périphériques standard des coeur arm cortex-m
    2. les crates de description matériel (PAC pour Peripherals Abstraction Crate). Ils gèrent le mapping des périphériques du microcontrôleur. Ils sont donc spécifique à la cible. ils nous permettent d'accéder aux registres de chaque périphérique par un ensemble de fonction/structure de données plutôt qu'en spécifiant des adresses mémoire.
    3. Les crates d'abstraction matériel (HAL pour Hardware Abstraction Layer). Ils sont basés sur les PAC et offrent une interface haut niveau et standardisée pour configurer et intéragire avec les périphériques.

    Dans cette première partie, nous n'utiliserons pas de HAL.

    Les crates

    Les crates est le nom donné en rust aux dépendances du projet. Elles sont
    la plupart du temps disponibles sur le site crates.io,
    mais on peut aussi
    spécifier des dépendances locales ou des répertoires git.

    Pour utiliser une crate, il y a deux possibilités:

    1. via cargo avec cargo add le_nom_du_crate Cargo va alors chercher le crate en question sur crates.io et s'il existe, il va ajouter la dépendance dans le fichier Cargo.toml et la télécharger dans le cache local.
    2. En éditant le fichier Cargo.toml.

    Dans le cas de notre mcu, il s'agit d'un coeur cortex M0+, nous allons donc
    utiliser les crates suivant:

    • cortex-m pour la description des périphériques liés au coeur arm.
    • cortex-m-rt pour la gestion de la compilation et de l'édition de liens.
    • panic-abort pour avoir une implémentation par défaut de la gestion des erreurs non recouvrable.
    • stm32l0 pour la description des périphériques du stm32l031.
    • stm32l0xx-hal pour la HAL (nous l'utiliserons dans un second temps).

    Pour les crates cortex-m, stm32l0 nous devons
    spécifier des options pour pouvoir les utiliser.

    Ce qui nous donne la liste de dépendance suivante:

    [dependencies]
    cortex-m-rt = "0.7.5"
    panic-abort = "0.3.2"
    cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
    stm32l0 = { version = "0.16.0", features = ["stm32l0x1", "rt"]}

    Édition de liens

    Le crate cortex-m-rt fournis un script d'édition de liens par défaut. Il nous
    réclame simplement de créer un fichier memory.x à la racine du projet
    indiquant la configuration de la *RAM
    et la FLASH.
    Pour ce qui est de la configuration de la table des vecteurs d'interruption,
    le crate cortex-m-rt offre un mécanisme permettant à l'utilisateur ou à d'autre
    crate d'insérer leur définition.
    Dans notre cas c'est le crate stm32l0 qui s'en charge via la feature rt.

    En ce qui concerne la configuration mémoire,
    notre micro (stm32l031k6) dispose de 32ko de flash et 8ko de ram.
    On va donc créer un fichier memory.x contenant:

    MEMORY
    {
      RAM  (rwx) : ORIGIN = 0x20000000, LENGTH = 8K
      FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 32K
    }
    

    Il ne nous reste plus qu'à éditer le fichier de config de cargo (.cargo/config.toml)
    pour ajouter un flag de compilation indiquant à rustc d'utiliser le script
    d'édition de liens du crate cortex-m-rt nommé link.x.

    [build]
    target = "thumbv6m-none-eabi"
    
    # flag pour l'édition de lien
    [target.thumbv6m-none-eabi]
    rustflags = ["-C", "link-arg=-Tlink.x",]

    Pour aller plus loin

    • Si on le souhaite, on peut aussi utiliser un script de liens personnel (doc).
    • Si on souhaite analyser le fichier elf avec objdump ce morceau de doc explicite les différentes sections.
    • script de liens par défaut: link.x

    Un simple clignotement

    Commençons par le hello world de l'embarqué: faire clignoter une led…

    On commence par créer un dossier: mkdir rust_stm32l031 && cd rust_stm32l031.

    Ensuite on initialise le projet avec cargo: cargo init --bin. Cette commande
    va créer les fichiers de config (Cargo.toml et Carlo.lock) ainsi qu'un répertoire
    src et le fichier main.rs.

    Mise en place de la fonction main

    Pour pouvoir compiler notre projet pour la cible stm32l0, on doit
    spécifier que le programme n'utilise pas la bibliothèque standard.
    Pour cela on utilise la directive: #![no_std]

    On doit aussi indiquer au compilateur que l'on souhaite utiliser
    une fonction de démarrage custom (fonction qui initialise les
    variables et appelle la fonction main). En effet, notre main()
    sera appelée depuis la fonction Reset implémentée par cortex-m-rt.
    Pour ce faire on utilise la directive: #![no_main].

    De son côté, le crate cortex-m-rt expose la directive #[entry]
    pour spécifier la fonction d'entrée.

    Enfin, on doit indiquer au compilateur comment la programme doit
    réagir en cas de détection de comportement interdit (panic).
    Pour ce faire, le compilateur attend une fonction:

    #[panic_handler]
    fn panic( info: &PanicInfo ) -> !{}

    Plutôt que de créer notre propre implémentation, on utilise le crate panic_abort
    afin qu'en cas d'erreur on déclenche un hardfault.

    On va donc avoir un fichier main.rs qui ressemble à ça:

    #![no_main]
    #![no_std]
    
    use cortex_m_rt::entry;
    user panic_abort as _;
    
    #[entry]
    fn main() -> ! {
     loop{}
    }

    Voila vous pouvez maintenant compiler et flasher ce programme sur
    votre carte électronique. Ça n'a aucun intérêt puisque ce programme ne fait rien
    mais ça fonctionne!

    Configurer les périphériques

    Nous avons mis en place notre chaîne de compilation, nous pouvons
    maintenant attaquer la configuration des périphériques. Pour cela,
    nous allons utiliser deux crates:

    • cortex-m pour l'accès aux périphériques arm, par exemple: systick, nvic …
    • stm32l0 pour l'accès aux périphériques stm32, par exemple: adc, gpio …

    Les objectifs sont:

    • configurer l'horloge pour utiliser le HSI 16MHz comme source
    • configurer le GPIO B3 en sortie pour faire clignoter une led
    • configurer le Systick pour cadencer le clignotement de la led

    Ce qui donne le fichier main.rs suivant:

    #![no_std]
    #![no_main]
    
    use cortex_m::{Peripherals, delay};
    use cortex_m_rt::entry;
    use panic_abort as _;
    use stm32l0::stm32l0x1;
    
    #[entry]
    fn main() -> ! {
        // acces aux périphériques
        let peripherals = stm32l0x1::Peripherals::take().unwrap();
        // on récupère le périphérique d'horloge et on configure le HSI
        let rcc = peripherals.RCC;
        rcc.apb2enr().write(|w| w.syscfgen().enabled());
        rcc.cr().write(|w| w.hsi16on().enabled());
        while rcc.cr().read().hsi16rdyf().bit_is_clear() {}
        rcc.cfgr().write(|w| w.sw().hsi16());
    
        // configuration du Systick pour une clock à 16 MHz et
        // utilisation de la structure delay de cortex-m
        let peripheral = Peripherals::take().unwrap();
        let mut delay = delay::Delay::new(peripheral.SYST, 16_000_000);
    
        // on active l'horloge pour le GPIOB
        rcc.iopenr().write(|w| w.iopben().enabled());
    
        // On configure le GPIOB3 en sortie push-pull
        let gpiob = peripherals.GPIOB;
        gpiob.moder().write(|w| w.mode3().output());
    
        loop {
            // on commence par lire l'état de la sortie via 'r' -> r.od3().bit()
            // On inverse la valeur à écrire par rapport à la valeur lu via '!'
            // et on écrit met à jour la sortie 'w' -> w.od3().bit(x)
            gpiob.odr().modify(|r, w| w.od3().bit(!r.od3().bit()));
            // attente de 500 ms
            delay.delay_ms(500);
        }
    }

    Et voilà un clignotement de led en bonne et due forme.

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.