C und C++ sind seit Jahrzehnten die Sprache der Mikrocontroller, abgesehen von der einen oder anderen handoptimierten Assemblerfunktion. Welche Chancen hat Rust also, welche Vorteile bietet es, und sollten Sie diese neue Sprache lernen?

Seit Jahrzehnten ist C die Sprache der Wahl für embedded Systeme. Dank seiner Fähigkeit, direkt auf den Speicher zuzugreifen und diesen zu manipulieren, hat es sich als Alternative zu Assembler durchgesetzt. Dank Arduino hat C++ auch bei kleineren Mikrocontrollern Einzug gehalten und einen objektorientierten Ansatz für die Entwicklung von Bibliotheken gefördert. Entwickler haben von einer Vielzahl von Bibliotheken profitiert, die leicht integriert und eingesetzt werden können und USB-Anwendungen, drahtlose Protokolle und die Interaktion mit externen Sensoren unterstützen.

Aber diese Sprachen haben auch ihre Grenzen. Sie eignen sich hervorragend für die unterste Ebene, bieten aber keine Unterstützung auf hoher Ebene, z. B. für die Dekodierung von JSON oder XML. Umgebungen wie Arduino machen die gemeinsame Nutzung von Bibliotheken einfach; ansonsten gibt es keine zentralen Repositorien für C/C++-Bibliotheken mit formalisierten Anwendungsprogrammierschnittstellen (API). Und als Embedded-Programmierer käme man nie auf die Idee, die verfügbaren Speicherzuweisungsbibliotheken oder Funktionen wie printf zu verwenden.

Abonnieren
Tag-Benachrichtigung zu Rust jetzt abonnieren!


Und dann gibt es noch all die fantastischen Fehler, die man mit Zeigern und nicht initialisierten variablen machen kann. Dank dieser linguistischen Fähigkeit können Programmierer auf jede Variable, Funktion oder jedes Register zugreifen, sofern die Hardware des Mikrocontrollers dies nicht durch Sicherheitsmaßnahmen verhindert. Und obwohl dies manchmal ein Geschenk des Himmels ist, kann eine falsch formulierte Codezeile haufenweise schwierige Probleme verursachen.

Was ist Rust?

Rust ist eine relativ neue, universell einsetzbare Programmiersprache, die von Graydon Hoare entwickelt wurde. Das Projekt begann im Jahr 2006 während seiner Zeit bei Mozilla und wurde später zu einem eigenständigen Projekt. Es wurde für die Entwicklung von Software für Systeme wie DNS, Browser und Virtualisierung konzipiert und wurde auch für einen Tor-Server verwendet. Rust verfolgt mehrere Ziele, aber das vielleicht wichtigste ist der eingebaute Umgang mit Speichersicherheit.

Wenn zum Beispiel eine Zeigervariable in C initialisiert wird, sollte ihr NULL zugewiesen werden (technisch als Null-Pointer-Konstante bekannt), wenn der tatsächliche Wert derzeit nicht verfügbar ist. Funktionen, die Zeiger verwenden, sollten auf NULL prüfen, bevor sie versuchen, eine Variable oder einen Funktionszeiger zu verwenden. Dies wird jedoch entweder nicht getan, oder die Programmierer vergessen, diese Prüfung vorzunehmen. Es gibt auch Mikrocontroller, wofür 0 (der Zahl 0), der Wert von NULL, eine gültige Speicher- oder Codestelle ist.
 
#include <stdio.h>
int main() {
   int *p= NULL;    //initialize the pointer as null.
   printf("The value of pointer is %u",p);
   return 0;
}

In Rust gibt es NULL nicht. Stattdessen gibt es ein Enum (enumerated type), das entweder einen Wert oder keinen Wert enthält. Er kann nicht nur mit Zeigern verwendet werden, sondern auch in vielen anderen Fällen, z. B. als Rückgabewert einer Funktion, die nichts (nicht 0) zurückgeben kann. Dies wird in der folgenden Divide-Funktion demonstriert, die mögliche Situationen, in denen durch 0 geteilt werden muss, sauber behandelt.
 
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

// The return value of the function is an option
let result = divide(2.0, 3.0);

// Pattern match to retrieve the value
match result {
    // The division was valid
    Some(x) => println!("Result: {x}"),
    // The division was invalid
    None    => println!("Cannot divide by 0"),
}

Code example from: https://doc.rust-lang.org/stable/std/option/index.html

In dieser Divide-Funktion bedeutet der Rückgabewert None, wenn der Nenner 0,0 ist, dass keine Antwort gegeben werden kann. Der Code, der die Funktion aufruft, kann auf den Rückgabewert None oder Some(T), d. h. auf einen gültigen Antwortwert, prüfen.

Die Typsicherheit ist ebenfalls sehr streng und stellt sicher, dass die Datentypen von Variablen mit denen anderer Variablen oder Literale übereinstimmen, die der Programmierer ihnen zuzuweisen versucht. So wird der Versuch, ein Integer implizit zu einem String hinzuzufügen, bei der Kompilierung als Fehler angezeigt. Ein weiteres Ziel der Sprache ist die Concurrency. Dies bedeutet, dass der Compiler die Reihenfolge, in der der Code ausgeführt wird, ändern kann. So kann ein Code beispielsweise aus einer Addition, einer Subtraktion und dem Kosinus des Winkels (in dieser Reihenfolge) bestehen. Rust kann diese Berechnung neu anordnen, wenn das korrekte Ergebnis immer noch erzielt werden kann. Diese Fähigkeit zielt jedoch auf Multicore- und Multiprozessorsysteme ab und ist für kleine embedded Systeme weniger geeignet.

Kann ich Rust mit meinem Mikrocontroller verwenden?

Theoretisch ja, aber es gibt einige praktische Hürden. Die erste ist die Toolchain. Die meisten Entwickler werden gcc verwendet haben, um ihren C-Code zu kompilieren. Rust verwendet jedoch LLVM. LLVM ist kein Compiler, sondern ein Framework für die Erstellung von Compilern. Mit Clang und LLVM ist es beispielsweise möglich, C-Code für einen Mikrocontroller zu kompilieren. Viele Hersteller sind zu einer LLVM-basierten Toolchain übergegangen, insbesondere diejenigen, die Arm Cortex-Prozessoren anbieten. Wenn es für Ihr bevorzugte Processor keine LLVM-Unterstützung gibt, werden Sie in absehbarer Zeit nicht mit Rust arbeiten können.
 
Slide1.PNG
Mit LLVM erzeugen verschiedene sprachspezifische Frontends LLVM IR-Output, die dann in den Maschinencode des Zielprozessors, z. B. Arm Cortex-M, konvertiert werden.
Die nächste Herausforderung ist die Beschaffung des Codes, der den Zugriff auf die Peripherieregister in Rust ermöglicht. Dies bringt uns zu einer Diskussion über Crates.

Während der Code in Rust geschrieben wird, sind Crates das, was alles zusammenfasst. Ein Crate enthält den Quellcode und die Konfigurationsdateien für Ihr Projekt. Und wenn Sie das Support-Paket für ein Entwicklungsboard, wie das micro:bit, benötigen, brauchen Sie dessen Crate. Es gibt auch ein Crate für die Peripherie des Nordic nRF51 Mikrocontrollers auf diesem Board. Schließlich gibt es noch ein Crate speziell für den Arm Cortex-M-Prozessor. 

Ein weiterer Vorteil von Crates ist, dass bereits eine Menge Code für allgemeine Aufgaben wie die Implementierung von I2C oder die Anbindung von SPI-Sensoren verfügbar ist. Mit dem Crate-Ansatz können Sie alle in Ihrem Projekt verwendeten Crates auflisten und sogar ihre Versionsnummer notieren, damit andere wissen, welche Version bei der Erstellung des Projekts verwendet wurde. Crates werden in der Regel in einem zentralen Online-Repository (crates.io), gespeichert, sodass sie leicht zu beschaffen sind.
 
Rust - generic dev board v4
Mit Rust bündeln Crates den Zugriff auf Prozessorregister, Mikrocontroller-Peripherieregister und sogar Ressourcen wie einen I2C-Temperatursensor auf Ihrem Entwicklungsboard

Welche anderen coolen Sachen sind in Rust eingebaut?

Das Rust-Ökosystem bietet noch viele andere interessante Extras, von der Sprache bis zu den Werkzeugen.

Literals, also die Werte, die wir Variablen und Konstanten zuweisen, sind oft schwer zu entziffern, vor allem, wenn die Sehkraft nachlässt, sei es durch das Alter oder durch die Anzahl der Bildschirmstunden, die man an diesem Tag investiert hat. Binäre, hexadezimale und sogar große ganze Zahlen und Konstanten können ungewollt eine Zahl oder Dezimalstelle gewinnen oder verlieren. Rust geht dieses Problem an, indem Unterstriche verwendet werden können, um den Wert in überschaubare Teile aufzuteilen. Der Unterstrich dient lediglich der besseren Lesbarkeit und hat keine weitere Funktion. Der Typ kann auch mit einem Suffix definiert werden.
 
10000 => 10_000
0.00001 => 0.000_01
0xBEEFD00F => 0xBEEF_D00Fu32 (32-bit unsigned integer)
0b01001011 => 0b0100_1011

Further examples: https://doc.rust-lang.org/book/ch03-02-data-types.html

Interessanterweise können Integers in Rust überlaufen, aber dieses Verhalten wird während der Kompilierung überwacht. Überläufe verursachen eine "Panic", wenn sie im Debug-Modus kompiliert werden, dürfen aber im Release-Modus existieren. Einige Methoden können jedoch explizite Überläufe, Wrapping oder Sättigung unterstützen. Es ist auch möglich, bei einem Überlauf None zurückzugeben, indem man die „checked Method“ verwendet.

Neben der Sprache gibt es auch eine Reihe von nützlichen Werkzeugen. Cargo ist sowohl der Build- als auch der Paketmanager. Rustfmt formatiert Ihren Quellcode mit der richtigen Einrückung. Dann gibt es noch Clippy, ein Lint-Tool, das eine statische Code-Analyse durchführt und seltsame Code-Konstrukte und mögliche Fehler aufspürt.

Und schließlich werden Entwickler zweifellos vorhandenen C/C++-Code integrieren wollen. Dies ist dank des Foreign Function Interface (FFI) möglich.
 
use libc::size_t;

#
extern {
    fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}

fn main() {
    let x = unsafe { snappy_max_compressed_length(100) };
    println!("max compressed length of a 100 byte buffer: {}", x);
}

Example for calling C function "snappy" from Rust sourced from: https://doc.rust-lang.org/nomicon/ffi.html
 

Wie kann ich Rust ausprobieren?

Wie die meisten Projekte sind auch die Werkzeuge für Rust open-source und frei verfügbar. Darüber hinaus gibt es zahlreiche Websites mit Dokumentation, Tutorials und Anleitungen. Der einfachste Ausgangspunkt ist vielleicht Ihr PC. Tutorials führen Sie durch den Prozess der Installation der Toolchain, gefolgt von einer Einführung in die Sprache.

Wenn Sie die Fähigkeiten von Rust im Bereich der embedded Systeme verstehen wollen, gibt es mehrere Möglichkeiten. Rust läuft auf dem Raspberry Pi und kann die verfügbaren Schnittstellen steuern, sodass eine LED relativ einfach umgeschaltet werden kann. Alternativ können Sie auch Code für den micro:bit erstellen und die STM32-Familie wird gut unterstützt. Wenn Sie keine Hardware zur Hand haben, können Sie versuchen, einen emulated microcontroller using QEMU.

Wenn Sie neugierig sind, ob es Echtzeit-Betriebssysteme (RTOS) gibt, können Sie sich Bern oder OxidOS ansehen (das Stuart Cording ebenfalls auf der embedded world interviewt hat). Es gibt auch eine Möglichkeit Rust-Code mit zugang zu FreeRTOS. zu versehen. Eine Liste von Projekten finden Sie unter https://arewertosyet.com/.

Wird Rust C ablösen für embedded Systeme?

Wenn Sie eine Karriere im embedded Software in Erwägung ziehen, könnten Sie sich natürlich Sorgen machen, dass Sie die falsche Sprache lernen. Es gibt jedoch keinen Grund zur Panik. Die Branche hatte jahrzehntelang Zeit, sich ähnliche Vorteile wie Rust durch Ada zu erlangen, aber diese Sprache hat sich nur in der Luft- und Raumfahrtindustrie durchgesetzt. Die Welt der embedded Systeme hat traditionell nur langsam Praktiken übernommen, die in anderen Zweigen der Softwareindustrie gang und gäbe sind, sodass die Chancen auf einen erdbewegenden Wechsel zu Rust im nächsten Jahrzehnt gering sind.

Trotz dieser Aussichten kann es nie schaden, seinen Horizont zu erweitern, insbesondere wenn man noch Jahrzehnte der Karriere vor sich hat. Dank des Internets, frei verfügbarer Ressourcen und des allgemeinen Zeitgeistes ist Ihr erstes Rust-Projekt vielleicht näher als Sie denken.

Want to Publish an Article in Elektor Mag? Here's How