Rust sur mcu: Écriture d'un driver

Sommaire

Intro

Suite à l’article précédent où nous avons mis en place l’environnement de développement et pris en main les crates de base, nous continuons avec à la conception d’un driver très simple.

Les objectifs sont:

Pour ce faire nous allons implémenter une api de délai non bloquant en s’appuyant 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 la suite nous utiliserons 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{}

Nous commençons 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 nous permet d’utiliser l’interruption du systick comme repère temporel.

Création de l’interface

Maintenant que le systick est prêt, nous pouvons implémenter notre driver. Pour cela, nous avons 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, nous proposons la structure suivante:

pub struct Delay{
	end_tick: u32,
}

Cette structure nous permet de stocker la consigne de limite temporelle. Pour faciliter son utilisation nous ajoutons 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 nous devions écrire ce code en C, nous nous contenterions 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++;
}

Ce que nous traduisons naïvement en rust part:

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 que 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.