C en C++ zijn al tientallen jaren de taal van microcontrollers, afgezien van de vreemde handmatig geoptimaliseerde assembler-functie. Dus welke kans maakt Rust, welke voordelen biedt het, en moet je deze nieuwe taal gaan leren?

Al tientallen jaren is C de favoriete programmeertaal voor embedded systemen. Dankzij de mogelijkheid om het geheugen direct te benaderen en te manipuleren is het omarmd als alternatief voor assembler. C++ heeft dankzij Arduino ook invloed gehad op kleinere microcontrollers, waarbij een objectgeoriënteerde benadering van het ontwerp van bibliotheken werd aangemoedigd. Ontwikkelaars hebben geprofiteerd van een groot aantal bibliotheken die gemakkelijk kunnen worden geïntegreerd en ingezet, met ondersteuning voor USB-toepassingen, draadloze protocollen en interactie met externe sensoren

ersteuning voor USB-toepassingen, draadloze protocollen en interactie met externe sensoren. Maar deze talen hebben hun beperkingen. Ze zijn geweldig op een heel laag niveau, maar missen daardoor ondersteuning op hoog niveau voor zaken als het decoderen van JSON of XML. Omgevingen als Arduino maken het delen van bibliotheken eenvoudig; aan de anderre kant zijn er geen centrale repositories voor C/C++ bibliotheken met geformaliseerde application programming interfaces (API). En als embedded programmeur zou je er nooit aan denken om de beschikbare bibliotheken voor geheugentoewijzing of functies als printf te gebruiken.

Inschrijven
Schrijf u in voor tag alert e-mails over Rust!

Dan zijn er nog al die grandioze fouten die je kunt maken met pointers en niet-geïnitialiseerde variabelen. Dankzij deze taalkundige eigenschappen hebben programmeurs toegang tot elke variabele, functie of register, tenzij de hardware van de microcontroller dit verhindert door veiligheidsmaatregelen. En hoewel dit soms een uitkomst is, kan een verkeerd opgestelde regel code een hoop uitdagende problemen veroorzaken.

Wat is Rust?

Rust is een relatief nieuwe programmeertaal voor algemeen gebruik, ontwikkeld door Graydon Hoare. Het project begon in 2006 tijdens zijn tijd bij Mozilla en werd later een zelfstandig project. Het is ontworpen voor het ontwikkelen van software voor systemen zoals DNS, browsers en virtualisatie, en is ook gebruikt voor een Tor-server. Rust heeft verschillende doelen, maar het belangrijkste is misschien wel de ingebouwde benadering van geheugenbeveiliging.

Bij het initialiseren van een pointervariabele in C moet deze bijvoorbeeld als NULL (technisch bekend als een null pointer-constante) worden toegewezen als de werkelijke waarde op dat moment niet beschikbaar is. Functies die pointers gebruiken moeten deze controleren op NULL voordat ze proberen een variabele of functie-pointer te gebruiken. Dit wordt echter ofwel niet gedaan, ofwel vergeten programmeurs domweg de controle toe te voegen. Er zijn ook microcontrollers waarvoor 0 (nul), de waarde van NULL, een geldige geheugen- of codepositie is.
 
#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 bestaat NULL niet. In plaats daarvan bestaat er een enum ("enumerated type") dat ofwel een waarde ofwel geen waarde bevat. Het kan niet alleen gebruikt worden met pointers, maar ook in veel andere gevallen, zoals een returnwaarde van een functie die niets (in plaats van 0) heeft om te retourneren. Dit wordt gedemonstreerd in de volgende delingsfunctie die netjes omgaat met potentiële situaties van delen door nul.
 
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

Als in deze delingsfunctie de noemer 0,0 is, geeft de retourwaarde None aan dat er geen antwoord kan worden gegeven. De code die de functie aanroept, kan controleren op een retourwaarde None of Some(T), d.w.z. een geldige waarde.

De type-veiligheid is ook zeer strikt, en zorgt ervoor dat de gegevenstypen van variabelen overeenkomen met die van andere variabelen of literals die de programmeur eraan probeert toe te wijzen. Zo wordt het impliciet toevoegen van een geheel getal aan een string tijdens het compileren gemarkeerd als een fout. Een ander doel van de taal is concurrency. Dit is de mogelijkheid voor de compiler om de volgorde waarin code wordt uitgevoerd te veranderen. Sommige programmaregels bevatten bijvoorbeeld een optelling, een aftrekking en de cosinus van de hoek (in deze volgorde). Rust kan deze berekening herschikken als het juiste resultaat nog steeds kan worden bereikt. Deze mogelijkheid is echter gericht op multi-core en multi-processor systemen en is minder geschikt voor kleine embedded systemen.

Kan ik Rust gebruiken op mijn microcontroller?

Theoretisch wel, maar er zijn een paar praktische hindernissen. De eerste is de toolchain. De meeste ontwikkelaars zullen gcc gebruikt hebben om hun C-code te compileren. Rust gebruikt echter LLVM. In plaats van een compiler is LLVM een raamwerk voor het maken van compilers. Door bijvoorbeeld Clang samen met LLVM te gebruiken, is het mogelijk om C-code voor een microcontroller te compileren. Veel fabrikanten zijn overgestapt op een op LLVM gebaseerde toolchain, vooral degenen die Arm Cortex-processoren aanbieden. Als er geen LLVM ondersteuning is voor jouw favoriete systeem, zul je Rust niet snel gebruiken.
 
Slide1.PNG
Met LLVM genereren diverse taalspecifieke front ends LLVM IR-uitvoer die
vervolgens wordt omgezet naar de machinetaal van de beoogde processor, bijvoorbeeld Arm Cortex-M.
De volgende uitdaging is het sourcen van de code die toegang geeft tot de perifere registers in Rust. Dit brengt ons bij een discussie over crates.

Terwijl de programmacode in Rust wordt geschreven, zijn het de crates die alles verpakken. Een crate bevat de broncode en configuratiebestanden voor je project. En als je de bijbehorende tools voor een ontwikkelboard wilt, zoals de micro:bit, dan wil je de bijbehorende crate. Er is ook een crate voor de randapparatuur van de Nordic nRF51 microcontroller serie op dat board. Tenslotte is er een crate speciaal voor de Arm Cortex-M processor die de micro:bit aanstuurt. 

Een ander leuk aspect van crates is dat er al veel code beschikbaar is voor algemene taken, zoals het implementeren van I2C, of het interfacen met SPI sensoren. Met de crate benadering kun je een lijst maken van alle crates die in je project worden gebruikt, en zelfs hun versienummer noteren, zodat anderen weten welke versie werkte toen het project werd gemaakt. Crates worden meestal opgeslagen in een centraal, online archief (crates.io), zodat ze gemakkelijk te verkrijgen zijn.
 
Rust - generic dev board v4
Met Rust bundelen crates de toegang tot processorregisters, perifere registers van de microcontroller,
en zelfs hulpmiddelen zoals een I2C temperatuursensor op je ontwikkelboard.

Welke andere interessante dingen zijn ingebouwd in Rust?

In het ecosysteem van Rust zijn tal van andere interessante extra's ingebouwd, van de programmeertaal tot en met de tools.

Literals, de waarden die we toekennen aan variabelen en constanten, zijn vaak moeilijk te ontcijferen, vooral als je gezichtsvermogen begint af te nemen, hetzij door ouderdom, hetzij door het aantal beeldschermuren dat je die dag hebt geïnvesteerd. Binaire, hexadecimale en zelfs grote gehele getallen en constanten kunnen onbedoeld een extra cjfer toegevoegd krijgen of er eentje verliezen, of de decimale positie kan veranderd worden. Rust pakt dit aan door het gebruik van underscores toe te staan om de waarde op te delen in hapklare brokken. De underscore wordt alleen gebruikt om de leesbaarheid te verbeteren en vervult geen andere rol. Het type kan ook worden gedefinieerd met een achtervoegsel.
 
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

Interessant is dat integers een overflow kunnen hebben in Rust, maar deze gebeurtenis wordt gecontroleerd tijdens het compileren. Overflows veroorzaken een 'panic' bij het compileren in debug-modus, maar mogen bestaan in release-modus. Sommige methoden kunnen echter expliciete overflows, wrapping of saturation ondersteunen. Het is ook mogelijk om bij een overflow None te retourneren met behulp van de gecontroleerde methode.

Behalve de taal zijn er ook een heleboel nuttige hulpmiddelen. Cargo is zowel de ontwerp- als package manager. Rustfmt formatteert je broncode met de juiste inspringingen. Dan is er Clippy, een linting tool die statische code-analyse uitvoert, op zoek naar vreemde codeconstructies en mogelijke bugs.

Tenslotte zullen ontwikkelaars ongetwijfeld bestaande C/C++ code willen integreren. Dit kan dankzij de Foreign Function Interface (FFI).
 
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
 

Hoe kan ik Rust uitproberen?

Zoals de meeste projecten zijn de tools voor Rust open-source en vrij beschikbaar. Bovendien zijn er veel websites met documentatie, handleidingen en begeleiding. Het eenvoudigste startpunt is misschien wel je PC. Tutorials leiden je door het installatieproces van de toolchain, gevolgd door een inleiding in de programmeertaal.

Er zijn verschillende opties als je de mogelijkheden van Rust op het gebied van embedded systemen wilt begrijpen. Rust draait op de Raspberry Pi en kan de aanwezige interfaces aansturen, zodat een LED met relatief gemak kan worden geschakeld. Je kunt ook code schrijven voor de micro:bit en de STM32 familie wordt goed ondersteund. Als je geen hardware bij de hand hebt, kun je een geëmuleerde microcontroller met QEMU uitproberen.

Als je benieuwd bent of er real-time besturingssystemen (RTOS) zijn, kun je kijken naar Bern of OxidOS (die Stuart Cording ook interviewde op Embedded World). Er is ook een manier om Rust code te voorzien van toegang tot FreeRTOS. Kijk voor een lijst van projecten op https://arewertosyet.com/.

Zal Rust C vervangen voor embedded systemen?

Als je een carrière in embedded software overweegt, maak je je natuurlijk zorgen dat je de verkeerde programmeertaal leert. Er is echter geen reden tot paniek. De industrie heeft tientallen jaren de tijd gehad om via Ada voordelen te verwerven die vergelijkbaar zijn met die van Rust, maar die taal is pas echt doorgedrongen tot de luchtvaartindustrie. Traditioneel is de embedded wereld traag in het overnemen van praktijken die gangbaar zijn in andere takken van de software-industrie, dus de kans op een ingrijpende overschakeling naar Rust in het komende decennium blijft gering.

Ondanks dit vooruitzicht kan het nooit kwaad om je horizon te verbreden, zeker niet als je nog tientallen jaren carrière voor de boeg hebt. Dankzij het Internet, vrij beschikbare bronnen, en de algemene tijdgeest is je eerste Rust-project misschien dichterbij dan je denkt. 

Wil je een artikel publiceren in Elektor Mag? Het werkt als volgt


Vertaling: Hans Adams