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

Singletons

In der Softwaretechnik ist das Singleton-Muster ein Entwurfsmuster, das die Instanziierung einer Klasse auf ein einziges Objekt beschränkt.

Wikipedia: Singleton-Muster

Aber warum können wir nicht einfach globale Variable(n) verwenden?

Wir könnten alles als „public static“ definieren, etwa so:

static mut THE_SERIAL_PORT: SerialPort = SerialPort;

fn main() {
    let _ = unsafe {
        THE_SERIAL_PORT.read_speed();
    };
}

Dies bringt jedoch einige Probleme mit sich. Es handelt sich um eine veränderbare globale Variable, und der Zugriff auf solche Variablen ist in Rust stets „unsafe“. Zudem sind diese Variablen im gesamten Programm sichtbar, was bedeutet, dass der Borrow Checker nicht dabei helfen kann, die Referenzen und die Ownership dieser Variablen zu verfolgen.

Wie machen wir das in Rust?

Anstatt unser Peripheriegerät einfach als globale Variable zu definieren, könnten wir uns stattdessen dazu entschließen, eine Struktur – in diesem Fall mit dem Namen PERIPHERALS – zu erstellen, die für jedes unserer Peripheriegeräte ein Option<T> enthält.

struct Peripherals {
    serial: Option<SerialPort>,
}
impl Peripherals {
    fn take_serial(&mut self) -> SerialPort {
        let p = replace(&mut self.serial, None);
        p.unwrap()
    }
}
static mut PERIPHERALS: Peripherals = Peripherals {
    serial: Some(SerialPort),
};

Diese Struktur ermöglicht es uns, eine einzelne Instanz unserer Peripheriekomponente zu erhalten. Wenn wir versuchen, take_serial() mehr als einmal aufzurufen, wird unser Code eine Panic auslösen!

fn main() {
    let serial_1 = unsafe { PERIPHERALS.take_serial() };
    // Dies loest eine panic aus!
    // let serial_2 = unsafe { PERIPHERALS.take_serial() };
}

Obwohl die Interaktion mit dieser Struktur als unsafe gilt, benötigen wir – sobald wir über die darin enthaltene SerialPort-Instanz verfügen – weder unsafe-Code noch die PERIPHERALS-Struktur selbst.

Dies bringt einen geringen Laufzeit-Overhead mit sich, da wir die SerialPort-Struktur in eine Option einbetten und einmalig take_serial() aufrufen müssen; dieser geringe anfängliche Aufwand ermöglicht es uns jedoch, den Borrow-Checker im weiteren Verlauf des Programms voll zu nutzen.

Vorhandene Bibliotheksunterstützung

Obwohl wir oben unsere eigene Peripherals-Struktur erstellt haben, ist dies für Ihren Code nicht erforderlich; das cortex_m-Crate enthält ein Makro namens singleton!(), das diese Aufgabe für Sie übernimmt.

use cortex_m::singleton;

fn main() {
    // In Ordnung, wenn `main` nur einmal ausgefuehrt wird.
    let x: &'static mut bool =
        singleton!(: bool = false).unwrap();
}

cortex_m Dokumente

Wenn Sie zudem cortex-m-rtic verwenden, wird der gesamte Prozess der Definition und Bereitstellung dieser Peripheriekomponenten für Sie abstrahiert; stattdessen erhalten Sie eine Peripherals-Struktur, die eine Version aller von Ihnen definierten Elemente enthält, die nicht als Option<T> vorliegen.

// cortex-m-rtic v0.5.x
#[rtic::app(device = lm3s6965, peripherals = true)]
const APP: () = {
    #[init]
    fn init(cx: init::Context) {
        static mut X: u32 = 0;
         
        // Cortex-M-Peripherie
        let core: cortex_m::Peripherals = cx.core;
        
        // Geraetespezifische Peripheriegeraete
        let device: lm3s6965::Peripherals = cx.device;
    }
}

Aber warum?

Aber wie bewirken diese Singletons einen spürbaren Unterschied in der Funktionsweise unseres Rust-Codes?

impl SerialPort {
    const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;

    fn read_speed(
        &self // <------ Das ist wirklich, wirklich wichtig.
    ) -> u32 {
        unsafe {
            ptr::read_volatile(Self::SER_PORT_SPEED_REG)
        }
    }
}

Hier spielen zwei wichtige Faktoren eine Rolle:

  • Da wir ein Singleton verwenden, gibt es nur eine Möglichkeit bzw.

einen Ort, um eine SerialPort-Struktur zu erhalten.

  • Um die Methode read_speed() aufzurufen, benötigen wir den Besitz

einer SerialPort-Struktur oder eine Referenz darauf.

Zusammengenommen bedeuten diese beiden Faktoren, dass ein Zugriff auf die Hardware nur möglich ist, wenn wir die Anforderungen des Borrow-Checkers ordnungsgemäß erfüllt haben – das heißt, dass zu keinem Zeitpunkt mehrere veränderbare Referenzen auf dieselbe Hardware existieren!

fn main() {
    // Verweis auf `self` fehlt! Das wird nicht funktionieren.
    // SerialPort::read_speed();

    let serial_1 = unsafe { PERIPHERALS.take_serial() };

    // Sie koennen nur lesen, worauf Sie Zugriff haben
    let _ = serial_1.read_speed();
}

Behandle deine Hardware wie Daten

Da es zudem sowohl veränderbare als auch unveränderbare Referenzen gibt, lässt sich erkennen, ob eine Funktion oder Methode potenziell den Zustand der Hardware ändern könnte. Zum Beispiel:

Dies darf Hardware-Einstellungen ändern:

fn setup_spi_port(
    spi: &mut SpiPort,
    cs_pin: &mut GpioPin
) -> Result<()> {
    // ...
}

Das nicht:

fn read_button(gpio: &GpioPin) -> bool {
    // ...
}

Dies ermöglicht es uns, bereits zur Kompilierzeit – und nicht erst zur Laufzeit – festzulegen, ob Code Änderungen an der Hardware vornehmen darf oder nicht. Zu beachten ist, dass dies im Allgemeinen nur innerhalb einer einzelnen Anwendung funktioniert; da unsere Software für Bare-Metal-Systeme jedoch als eine einzige Anwendung kompiliert wird, stellt dies üblicherweise keine Einschränkung dar.