Journal Découverte de rust pour l'embarqué 2

Posté par  (site web personnel, Mastodon) . Licence CC By‑SA.
15
10
juin
2026

Sommaire

Suite à l'article précédent, je continue ma découverte de RUST pour l'embarqué. Cette fois-ci, je me suis intéressé à la gestion des interruptions. Pour appréhender ce problème, je me suis lancé dans l'implémentation d'une API de délais non bloquante basé sur le périphérique "SYSTICK".

L'idée est de créer une api qui ressemble à:

 let mut mon_timer = delay::new();

 mon_timer.start(250);

 loop{
     if mon_timer.expired() == true {
         mon_timer.start(543);   
         led_toggle();
     }
 }

Configuration du SYSTICK

Pour cet exercice, j'utiliserai le crate hal stm32l0-hal.

En partant du fichier main.rs suivant:

use cortex_m;
use cortex_m_rt::entry;
use panic_abort as _;
use stm32l0xx_hal::{pac, prelude::*, rcc::Config};

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();

    // On configure le quartz interne en source d'horloge
    let mut rcc = dp.RCC.freeze(Config::hsi16());

    // init du GPIO de la LED
    let gpiob = dp.GPIOB.split(&mut rcc);
    let mut led = gpiob.pb3.into_push_pull_output();
    led.set_high().unwrap();

    loop{}

On commence par configurer le périphérique systick pour que son compteur déborde tout les 1ms. Ce qui nous donne la fonction "main" suivante:

use cortex_m;
use cortex_m_rt::entry;
use panic_abort as _;
use stm32l0xx_hal::{pac, prelude::*, rcc::Config};

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let mut cp = cortex_m::Peripherals::take().unwrap();

    // On configure le quartz interne en source d'horloge
    let mut rcc = dp.RCC.freeze(Config::hsi16());

    // configuration du systick via l'api HAL
    // On le configure pour qu'il ait une cadence de 1ms
    cp.SYST.set_clock_source(cortex_m::peripheral::syst::SystClkSource::Core);
    cp.SYST.set_reload(16_000);
    cp.SYST.enable_counter();
    cp.SYST.enable_interrupt();
    cp.SYST.clear_current();
    while !cp.SYST.has_wrapped() {}

    // init du GPIO de la LED
    let gpiob = dp.GPIOB.split(&mut rcc);
    let mut led = gpiob.pb3.into_push_pull_output();
    led.set_high().unwrap();

    loop{}

Cette configuration me permet d'utiliser l'interruption du systick comme repère temporel.

Création de l'interface

Maintenant que le systick est prêt, on peut implémenter le driver. Pour cela, on a besoin de deux choses:

  1. une base de temps
  2. une consigne de temps limite

Pour ce qui est de la base temps, on utilisera l'interruption du systick. Et pour le temps limite, je propose d'utiliser la structure suivante:

pub struct Delay{
    end_tick: u32,
}

Cette structure permet de stocker la consigne de limite temporelle. Pour faciliter son utilisation j'ajoute les fonctions suivantes:

impl Delay {
    pub fn new() -> Delay {
        return Delay { end_tick: 0 };
    }

    pub fn is_elapse(&self) -> bool {
    // reach error not implemented
        return false;
    }

    pub fn start(&mut self, tick_to_wait: usize) {
    // reach error not implemented
        self.end_tick = tick_to_wait;
    }
}

Maintenant, passons à l'implémentation de la gestion du temps via l'interruption de systick.

Interruption de SYSTICK

Si je devais écrire ce code en C, je me contenterai sans doute de déclarer une variable globale et de l'incrémenter dans l'interruption. En utilisant une variable de type u32 on s'assure d'un accès atomique (pour notre cible) donc pas de risque de concurrence d'accès.

#include <stdint.h>

static volatile uint32_t tick  = 0u;

void Systick_IRQHandler(void){
    tick++;
}

En traduisant naïvement ce code en rust, ça donne:

pub use cortex_m_rt::exception;

static mut COUNT:u32 = 0;

pub struct Delay{
    end_tick: u32,
}

impl Delay {
    pub fn new() -> Delay {
        return Delay { end_tick: 0 };
    }

    pub fn is_elapse(&self) -> bool {
        return unsafe{self.end_tick >= COUNT};
    }

    pub fn start(&mut self, tick_to_wait: usize) {
        self.end_tick = unsafe{COUNT + tick_to_wait};
    }
}

// Interruption de SysTick. Pour que la fonction soit identifier
// comme une interruption, on utilise la directive *exception*
// de cortex-m-rt
#[exception]
fn SysTick() {
    unsafe {
        COUNT += 1_u32;
    }
}

En rust, la manipulation de variable globales nécessite de passer par des sections unsafe. Attention, il est cependant strictement interdit de déréférencer la variable COUNT. Il s'agit d'un comportement non définit en rust (UB).

Explications et détails

De plus, je ne sais pas si cette implémentation garantie que le compilateur n'optimisera pas l'accès à la variable COUNT (l'équivalent du volatile en C) mais une fois compiler et flasher sur le microcontrôleur, tout semble fonctionner. Sans doute un coup de chance…

Il existe cependant d'autres options permettant de ne pas passer par des sections unsafe. La première est d'expliciter l'atomicité de l'accès en utilisant: core::sync::atomic::{AtomicU32, Ordering}. Une autre est d'utiliser la structure mutex du crate cortex_m.

Mutex

Voici une implémentation de l'accès à une variable globale via le crate cortex_m::interrupt::Mutex`. Vous trouverez un exemple officiel ici.

pub use core::cell::RefCell;
pub use cortex_m::interrupt::Mutex;
pub use cortex_m_rt::exception;

static COUNT: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0));
pub struct Delay {
    end_tick: u32,
}

impl Delay {
    pub fn new() -> Delay {
        return Delay { end_tick: 0 };
    }

    pub fn is_elapse(&self) -> bool {
        let mut count: u32 = 0;
        cortex_m::interrupt::free(|cs| {
            count = *COUNT.borrow(cs).borrow();
        });
        return self.end_tick <= count;
    }

    pub fn start(&mut self, tick: u32) {
        cortex_m::interrupt::free(|cs| {
            self.end_tick = *COUNT.borrow(cs).borrow() + tick;
        });
    }
}

#[exception]
fn SysTick() {
    cortex_m::interrupt::free(|cs| {
        let mut count = COUNT.borrow(cs).borrow_mut();
        *count += 1;
    });
}

Cette solution offerte par le crate cortex_m est fonctionnel. Cependant, je trouve quelle alourdie pas mal le code. Pour quelqu'un qui vient du C, c'est même assez déroutant.

Atomic

Une autre solution consiste à passer par le type Atomic proposé par le crate core::sync::atomic. Il nous permet d'indiquer au compilateur que la variable qu'il manipule est accédée atomiquement. Ce qui nous donne le code suivant:

pub use core::sync::atomic::{AtomicUsize, Ordering};
pub use cortex_m_rt::exception;

static COUNT: AtomicUsize = AtomicUsize::new(0);
pub struct Delay {
    end_tick: usize,
}

impl Delay {
    pub fn new() -> Delay {
        return Delay { end_tick: 0 };
    }

    pub fn is_elapse(&self) -> bool {
        return self.end_tick <= COUNT.load(Ordering::Relaxed);
    }

    pub fn start(&mut self, tick: usize) {
        self.end_tick = COUNT.load(Ordering::Relaxed) + tick;
    }
}

fn SysTick() {
    COUNT.store(COUNT.load(Ordering::Relaxed) + 1, Ordering::Relaxed);
}

Je trouve cette solution bien plus facile à lire que celle utilisant le mutex.

Bilan

La première implémentation est certainement la plus intuitive pour quelqu'un ayant l'habitude du C, mais elle reste à éviter à cause de l'utilisation de sections unsafe. La troisième solution (atomic) est celle que je trouve la plus explicite et facile à lire. Cependant, ces deux solutions ne permettent de travailler qu'avec une variable à accès atomic. Si l'on souhaite éditer une variable plus complexe comme une structure, il faudra alors se tourner vers la solution mutex.

Enfin, un autre avantage des méthodes unsafe et atomic, est quelles ne sont pas dépendants de la cible. Contrairement à la méthode à base de mutex qui impose de compiler pour une cible cortex_m. Pour l'instant on s'en moque un peu mais lorsque l'on voudra utiliser la directive #cfg[test] afin de valider le fonctionnement des fonctions par des tests unitaires, ça prendra une tout autre importance.

Article original

  • # u32++ n'est pas atomique

    Posté par  (site web personnel) . Évalué à 7 (+5/-0).

    J'ai pas fais de système depuis longtemps, donc je me trompe peux-être, mais je pense que l'implementation initiale en C avec volatile est fausse:

    a) volatile n'est même pas forcement nécessaire ici. Le compilateur n'a pas de raison d'optimiser la lecture écriture d'une variable globale dans ce contexte. Si tu avais une boucle genre while(tick < 100) { }, alors, oui, volatile pourrait faire partie de la solution car le compilateur à le droit de considérer que tick ne change pas depuis l’extérieur. Mais dans ta fonction Systick_IRQHandler, le compilateur est forcé de lire et écrire la valeur globale. J'ai regardé dans goldbot, il n'y a aucune difference avec ou sans volatile: https://godbolt.org/z/3bW9PEPWP
    b) Sauf erreur de ma part, l'incrementation d'un entier n'est pas forcement atomique, semantiquement, cela reste un fetch; add; store. Sur x86_64, le add n'est pas atomique par défaut, il faut lui ajouter "lock" pour que cela le soit.

    Bref, si ici Rust est plus verbeux, c'est aussi parce que la solution "naive" en C est très certainement fausse. En C il faudra utiliser des atomic aussi (si disponible dans ta version de C) ou differentes primitives qui font principalement des barrier memoire.

    • [^] # Re: u32++ n'est pas atomique

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

      Merci pour ton commentaire. En effet, dans ce bout de code isolé le volatile n'est pas nécessaire. Mais l'idée était de faire une attente non bloquante, donc venir comparer régulièrement la valeur de COUNT avec une valeur de référence.

      Par exemple:

      static int count = 0;
      
      // IRQ
      void foo() {
          count+=1;
      }
      
      int main(){
          int a = 0;
          for(;;){
              if(count > 10){
                  a = 10;
              }
          }
          return 0;
      }

      Et dans ce cas le volatile me semble indispensable.

      Pour la remarque b, c'est aussi vrai. Cependant dans notre cas, il n'y a que l'interruption qui modifie la valeur de COUNT, tous les autres accès ne sont qu'en lecture.

      • [^] # Re: u32++ n'est pas atomique

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

        Merci de ta réponse.

        Je me demande si volatile est assez pour une boucle active, mais là j'avoue que je ne sais pas/plus. Par principe de sécurité, j'utiliserais toujours les opérations atomique, qui en plus de garantie l'atomicité, garantissent que les précautions suffisantes sont prises en fonction de l'architecture pour gerer les subtilités de la mémoire et du cache. Après, c'est peut-être pas pertinant sur un stm32 ;)

  • # lib ?

    Posté par  (site web personnel) . Évalué à 3 (+0/-0).

    Super serie !

    J ai une question : est-ce qu'il y a des fonctions de haut niveau bien faite ?

    Je pense par exemple au printf non bloquant que l on doit reimplementer sur chaque projet. Alors que le comportement read/write d'arduino est tres bien (ne bloque que si le buffer est plein).

    "La première sécurité est la liberté"

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.