URL:     https://linuxfr.org/users/unetanche/journaux/decouverte-de-rust-pour-l-embarque-2
Title:   Découverte de rust pour l'embarqué 2
Authors: uneTanche
Date:    2026-06-10T09:58:17+02:00
License: CC By-SA
Tags:    embarqué, rust et microcontrôleur
Score:   15


Suite à [l'article précédent](https://linuxfr.org/users/unetanche/journaux/decouverte-de-rust-pour-l-embarque), 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 à:

```rust
 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](https://github.com/stm32-rs/stm32l0xx-hal).

En partant du fichier `main.rs` suivant:

```rust
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:

```rust
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:

```rust
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:

```rust
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.

```c
#include <stdint.h>

static volatile uint32_t tick  = 0u;

void Systick_IRQHandler(void){
	tick++;
}
```

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

```rust
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](https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html)

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](https://docs.rust-embedded.org/book/concurrency/#mutexes).

```rust
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:

```rust
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](https://www.ylbn.fr/blog/rust_embedded_2)
