Une Tanche

Rust sur mcu: Découverte

Linker scripts
Sommaire

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:
    [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!

configuration des 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.