Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ein erster Versuch in Rust

Die Register

Betrachten wir die „SysTick“-Peripherie – einen einfachen Timer, der in jedem Cortex-M-Prozessorkern enthalten ist. Normalerweise würde man hierfür im Datenblatt des Chip-Herstellers oder im Technical Reference Manual nachschlagen; da dieses Beispiel jedoch für alle ARM-Cortex-M-Kerne gilt, werfen wir einen Blick in das ARM-Referenzhandbuch. Wir sehen, dass es vier Register gibt:

OffsetNameBeschreibungBreite
0x00SYST_CSRSteuerung und Status Register32 bits
0x04SYST_RVRWert-Register neu laden32 bits
0x08SYST_CVRRegister für den aktuellen Wert32 bits
0x0CSYST_CALIBKalibrierwert-Register32 bits

Der C-Ansatz

In Rust können wir eine Sammlung von Registern auf genau dieselbe Weise darstellen wie in C – mit einem struct.

#[repr(C)]
struct SysTick {
    pub csr: u32,
    pub rvr: u32,
    pub cvr: u32,
    pub calib: u32,
}

Der Qualifizierer #[repr(C)] weist den Rust-Compiler an, diese Struktur wie ein C-Compiler anzulegen. Das ist sehr wichtig, da Rust die Neuordnung von Strukturfeldern zulässt, C jedoch nicht. Sie können sich vorstellen, wie viel Debugging wir durchführen müssten, wenn diese Felder vom Compiler stillschweigend neu angeordnet würden! Mit diesem Qualifizierer verfügen wir über unsere vier 32-Bit-Felder, die der obigen Tabelle entsprechen. Aber diese „Struktur“ allein nützt natürlich nichts – wir brauchen eine Variable.

let systick = 0xE000_E010 as *mut SysTick;
let time = unsafe { (*systick).cvr };

Volatile-Zugriffe

Nun gibt es bei dem oben genannten Ansatz einige Probleme.

  1. Wir müssen unsafe verwenden, wann immer wir auf unsere Peripherie zugreifen wollen.
  2. Wir haben keine Möglichkeit festzulegen, welche Register nur lesbar oder sowohl les- als auch schreibbar sind.
  3. Jeder Code-Abschnitt an beliebiger Stelle Ihres Programms könnte über diese Struktur auf die Hardware zugreifen.
  4. Vor allem aber funktioniert es eigentlich nicht …

Das Problem ist nun, dass Compiler clever sind. Wenn man zwei Schreibzugriffe nacheinander auf denselben Speicherbereich durchführt, kann der Compiler dies erkennen und den ersten Schreibvorgang einfach komplett überspringen. In C können wir Variablen als volatile kennzeichnen, um sicherzustellen, dass jeder Lese- oder Schreibvorgang wie vorgesehen ausgeführt wird. In Rust hingegen kennzeichnen wir die Zugriffe als volatile, nicht die Variable.

let systick = unsafe { &mut *(0xE000_E010 as *mut SysTick) };
let time = unsafe { core::ptr::read_volatile(&mut systick.cvr) };

Wir haben also eines unserer vier Probleme gelöst, aber jetzt haben wir noch mehr unsafe-Code! Glücklicherweise gibt es ein Crate eines Drittanbieters, das hierbei helfen kann – volatile_register.

use volatile_register::{RW, RO};

#[repr(C)]
struct SysTick {
    pub csr: RW<u32>,
    pub rvr: RW<u32>,
    pub cvr: RW<u32>,
    pub calib: RO<u32>,
}

fn get_systick() -> &'static mut SysTick {
    unsafe { &mut *(0xE000_E010 as *mut SysTick) }
}

fn get_time() -> u32 {
    let systick = get_systick();
    systick.cvr.read()
}

Die volatilen Zugriffe erfolgen nun automatisch über die Methoden read und write. Schreibzugriffe sind zwar weiterhin als unsafe eingestuft, doch fairerweise muss man sagen, dass Hardware im Grunde aus veränderlichem Zustand besteht und der Compiler nicht wissen kann, ob diese Schreibvorgänge tatsächlich sicher sind; daher ist dies eine sinnvolle Standardeinstellung.

Der Rusty Wrapper

Wir müssen diese struct in eine API der höheren Ebene verpacken, deren Aufruf für unsere Nutzer sicher ist. Als Treiberentwickler überprüfen wir manuell die Korrektheit des unsafe-Codes und stellen unseren Nutzern dann eine sichere API zur Verfügung, sodass sie sich darum nicht kümmern müssen (vorausgesetzt, sie vertrauen darauf, dass wir alles richtig gemacht haben!).

Ein Beispiel hierfür wäre:

use volatile_register::{RW, RO};

pub struct SystemTimer {
    p: &'static mut RegisterBlock
}

#[repr(C)]
struct RegisterBlock {
    pub csr: RW<u32>,
    pub rvr: RW<u32>,
    pub cvr: RW<u32>,
    pub calib: RO<u32>,
}

impl SystemTimer {
    pub fn new() -> SystemTimer {
        SystemTimer {
            p: unsafe { &mut *(0xE000_E010 as *mut RegisterBlock) }
        }
    }

    pub fn get_time(&self) -> u32 {
        self.p.cvr.read()
    }

    pub fn set_reload(&mut self, reload_value: u32) {
        unsafe { self.p.rvr.write(reload_value) }
    }
}

pub fn example_usage() -> String {
    let mut st = SystemTimer::new();
    st.set_reload(0x00FF_FFFF);
    format!("Time is now 0x{:08x}", st.get_time())
}

Das Problem bei diesem Ansatz ist nun, dass der folgende Code für den Compiler völlig zulässig ist:

fn thread1() {
    let mut st = SystemTimer::new();
    st.set_reload(2000);
}

fn thread2() {
    let mut st = SystemTimer::new();
    st.set_reload(1000);
}

Das &mut self-Argument der Funktion set_reload stellt zwar sicher, dass keine weiteren Referenzen auf genau diese SystemTimer-Struktur existieren, verhindert jedoch nicht, dass der Benutzer einen zweiten SystemTimer erstellt, der auf dieselbe Peripherieeinheit verweist. Auf diese Weise geschriebener Code funktioniert zwar, sofern der Autor sorgfältig genug ist, all diese „doppelten“ Treiberinstanzen zu erkennen; sobald sich der Code jedoch über mehrere Module, Treiber, Entwickler und Zeiträume hinweg erstreckt, schleichen sich derartige Fehler immer leichter ein.